100. Case Study: A Complete Microservices Architecture with gRPC in Go
title: 100. Case Study: A Complete Microservices Architecture with gRPC in Go
author: Maya Wijaya
date: 2024-06-20
tags: [microservices, golang, grpc, arsitektur, studi kasus, kode, devops]
Introduction
In the modern era of software development, microservices architecture has become a standard widely adopted by technology companies both small and large. One of the main reasons is the ability of microservices to improve scalability, technology flexibility, and to speed up the deployment of new features. However, implementing microservices often introduces challenges related to inter-service communication, automation, and deployment orchestration. In this 100th article, I’ll walk through a complete case study of a microservices architecture using gRPC in Go, complete with code examples, flow simulations, and a development workflow.
Case Study: An Online Ordering System
As an example, we’ll build a basic online ordering system with three main services:
- User Service: manages user data
- Order Service: handles orders
- Product Service: manages product data
These three services communicate internally using gRPC.
Architecture Overview
Let’s start with the architecture diagram:
flowchart TD A[User Service] --gRPC--> B[Order Service] C[Product Service] --gRPC--> B[Order Service] D[Client (REST/GRPC)] --gRPC--> A[User Service] D --gRPC--> B D --gRPC--> C
From this diagram, the user, order, and product services all communicate peer-to-peer using gRPC.
Advantages of gRPC in Go Microservices
A few of the main reasons we choose gRPC for Go microservices:
- High Performance: gRPC uses Protocol Buffers, which are far faster and more efficient than JSON.
- Strict Typing: Minimizes bugs caused by type mismatches.
- Cross Language: gRPC is supported by many programming languages.
- API Contract: Services are clearly defined via
.protofiles, making them easy to integrate across teams.
1. Defining the API Contract: Protobuf
All communication begins with defining the API contract in a .proto file. Here’s an example order.proto file:
1syntax = "proto3";
2
3package order;
4
5service OrderService {
6 rpc CreateOrder (CreateOrderRequest) returns (CreateOrderResponse);
7 rpc GetOrder (GetOrderRequest) returns (GetOrderResponse);
8}
9
10message CreateOrderRequest {
11 string user_id = 1;
12 string product_id = 2;
13 int32 quantity = 3;
14}
15
16message CreateOrderResponse {
17 string order_id = 1;
18 string status = 2;
19}
20
21message GetOrderRequest {
22 string order_id = 1;
23}
24
25message GetOrderResponse {
26 string order_id = 1;
27 string user_id = 2;
28 string product_id = 3;
29 int32 quantity = 4;
30 string status = 5;
31}Each microservice has its own .proto file to describe its public interface.
2. Implementing the gRPC Service in Go
Once the contract is defined, we can generate the Go code from the .proto file above using a plugin such as:
1protoc --go_out=. --go-grpc_out=. order.protoExample Order Service Implementation
Let’s look at a simple gRPC server implementation of the OrderService:
1// order_service.go
2package main
3
4import (
5 "context"
6 pb "your_project/orderpb"
7 "log"
8 "net"
9
10 "google.golang.org/grpc"
11)
12
13type orderServer struct {
14 pb.UnimplementedOrderServiceServer
15 orders map[string]*pb.GetOrderResponse
16}
17
18func (s *orderServer) CreateOrder(ctx context.Context, req *pb.CreateOrderRequest) (*pb.CreateOrderResponse, error) {
19 orderID := "ORD12345" // simulated auto ID
20 order := &pb.GetOrderResponse{
21 OrderId: orderID,
22 UserId: req.UserId,
23 ProductId: req.ProductId,
24 Quantity: req.Quantity,
25 Status: "CREATED",
26 }
27 s.orders[orderID] = order
28 return &pb.CreateOrderResponse{
29 OrderId: orderID,
30 Status: order.Status,
31 }, nil
32}
33
34func (s *orderServer) GetOrder(ctx context.Context, req *pb.GetOrderRequest) (*pb.GetOrderResponse, error) {
35 if order, ok := s.orders[req.OrderId]; ok {
36 return order, nil
37 }
38 return nil, status.Errorf(codes.NotFound, "Order not found")
39}
40
41func main() {
42 lis, err := net.Listen("tcp", ":50052")
43 if err != nil {
44 log.Fatalf("failed to listen: %v", err)
45 }
46 grpcServer := grpc.NewServer()
47 pb.RegisterOrderServiceServer(grpcServer, &orderServer{orders: make(map[string]*pb.GetOrderResponse)})
48 log.Println("Order Service gRPC running on :50052")
49 grpcServer.Serve(lis)
50}Order Client Simulation
1// order_client.go
2package main
3
4import (
5 "context"
6 "log"
7 "time"
8
9 pb "your_project/orderpb"
10
11 "google.golang.org/grpc"
12)
13
14func main() {
15 conn, err := grpc.Dial(":50052", grpc.WithInsecure())
16 if err != nil {
17 log.Fatalf("did not connect: %v", err)
18 }
19 defer conn.Close()
20 client := pb.NewOrderServiceClient(conn)
21 ctx, cancel := context.WithTimeout(context.Background(), time.Second)
22 defer cancel()
23
24 // Create a new order
25 orderResp, err := client.CreateOrder(ctx, &pb.CreateOrderRequest{
26 UserId: "USR123",
27 ProductId: "PRD789",
28 Quantity: 2,
29 })
30 if err != nil {
31 log.Fatalf("CreateOrder failed: %v", err)
32 }
33 log.Printf("CreateOrder Response: OrderID=%s Status=%s", orderResp.OrderId, orderResp.Status)
34}3. Orchestrating the Microservices
In real-world practice, the three services (User, Order, Product) will run on different ports and hosts. For a local setup, we can use docker-compose.
Example docker-compose.yml
1version: "3.8"
2services:
3 user:
4 build: ./user-service
5 ports:
6 - "50051:50051"
7 order:
8 build: ./order-service
9 ports:
10 - "50052:50052"
11 product:
12 build: ./product-service
13 ports:
14 - "50053:50053"4. Simulating the Ordering Flow
Let’s illustrate the order process from the client all the way to completion using mermaid:
sequenceDiagram participant Client participant UserService participant OrderService participant ProductService Client->>UserService: Validate User UserService-->>Client: OK Client->>ProductService: Check Product & Stock ProductService-->>Client: OK Client->>OrderService: CreateOrder(UserID, ProductID, Qty) OrderService-->>Client: OrderID, Status
5. gRPC vs REST Comparison (Table)
| Feature | gRPC | REST |
|---|---|---|
| Payload | Protocol Buffers | JSON |
| Speed | Very fast | Slower |
| Contract | Strongly Typed | Flexible |
| Streaming | Yes | Difficult |
| Binary Support | Native | No |
| Language Support | Multi (Auto-gen) | All |
| Backward Comp | Easier | Depends |
6. Best Practices
A few tips for applying a gRPC microservices architecture in Go:
- Use context and timeout for all RPCs.
- Handle errors clearly in every service.
- Apply observability: tracing & monitoring (OpenTelemetry, Prometheus).
- Use a central registry/discovery service in production (e.g., Etcd, Consul).
- Apply good Protobuf versioning.
Conclusion
In this case study, we’ve seen how to build a microservices architecture with gRPC in Go—from defining the API contract and implementing the services, to orchestration and simulating the workflow between services. With gRPC, we gain better performance, security, and maintainability—making it a great fit for enterprise scale. For further development, feel free to add JWT authentication, a circuit breaker, and monitoring to each of your services!
Happy experimenting with modern microservices architecture!
References
Happy hacking 🍃