Go Microservice Architecture with gRPC
A complete guide to building high-performance backend microservices in Go using gRPC, Protocol Buffers, interceptors, streaming, and production best practices.
📌 What Is gRPC?
gRPC is an open-source, high-performance Remote Procedure Call (RPC) framework originally developed by Google in 2015. It lets you define service methods and message types in a .proto file, then auto-generates strongly-typed client and server code in multiple languages.
The "g" in gRPC doesn't stand for "Google" — it has a different meaning for each release version.
Core technologies powering gRPC:
| Technology | Role |
| HTTP/2 | Multiplexed connections, header compression, server push |
| Protocol Buffers (protobuf) | Binary serialization — compact, fast, language-neutral |
| Code generation | Auto-generated client/server stubs from .proto definitions |
⚡ Why gRPC Over REST for Microservices?
REST is excellent for public-facing APIs. But for internal service-to-service communication, gRPC offers significant advantages.
| Aspect | REST (HTTP/JSON) | gRPC (HTTP/2 + Protobuf) |
| Serialization | JSON (text-based, verbose) | Protobuf (binary, compact) |
| Performance | Baseline | ~7–10× faster throughput |
| Contract | OpenAPI/Swagger (optional) | .proto file (enforced) |
| Streaming | Websockets (separate layer) | Native bidirectional streaming |
| Code generation | Limited | First-class, multi-language |
| Browser support | Native | Requires gRPC-Web or gateway |
| HTTP version | HTTP/1.1 | HTTP/2 |
The practical recommendation: Use gRPC for internal service-to-service calls. Expose REST endpoints for browser clients and public API consumers using a gRPC-Gateway or API gateway layer.
🏗️ Architecture Overview
┌──────────────┐
│ API Gateway │ (REST ↔ gRPC translation)
│ / gRPC-GW │
└──────┬───────┘
│
┌──────────────────┼──────────────────┐
│ │ │
┌─────▼─────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ Service A │ │ Service B │ │ Service C │
│ (Go gRPC) │◄──►│ (Go gRPC) │◄──►│ (Go gRPC) │
└─────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
┌─────▼─────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ Database A │ │ Database B │ │ Message Q │
└───────────┘ └─────────────┘ └─────────────┘
All inter-service calls use gRPC over HTTP/2.
Each service owns its own data store.
.proto files define the contract between services.
📋 Prerequisites & Tooling Setup
1. Install the Protocol Buffer Compiler (protoc)
Linux (apt):
sudo apt install -y protobuf-compiler
macOS (Homebrew):
brew install protobuf
Verify the installation (you need version 3+):
protoc --version
# libprotoc 27.x
If neither package manager works, download precompiled binaries from the official protobuf releases.
2. Install Go Plugins for protoc
The protobuf compiler doesn't natively generate Go code — you need two plugins:
# Generates Go structs from .proto messages
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
# Generates Go gRPC service interfaces and stubs
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
Important: Make sure $GOPATH/bin is in your PATH:
export PATH="$PATH:$(go env GOPATH)/bin"
Add this line to your ~/.bashrc, ~/.zshrc, or equivalent shell profile so it persists.
3. Verify Everything
protoc --version # protoc compiler
protoc-gen-go --version # Go protobuf plugin
protoc-gen-go-grpc --version # Go gRPC plugin
📂 Project Structure
A clean, scalable Go gRPC microservice project follows this layout:
greeting-app/
├── go.mod
├── go.sum
├── proto/
│ ├── greeting.proto # Service + message definitions
│ └── greeting/
│ ├── greeting.pb.go # Auto-generated: message structs
│ └── greeting_grpc.pb.go # Auto-generated: gRPC service stubs
├── greeter-server/
│ └── main.go # Server implementation
└── greeter-client/
└── main.go # Client implementation
For production multi-service projects, a recommended structure:
my-platform/
├── proto/ # Shared .proto definitions
│ ├── user/
│ │ └── user.proto
│ ├── order/
│ │ └── order.proto
│ └── payment/
│ └── payment.proto
├── services/
│ ├── user-service/
│ │ ├── cmd/
│ │ │ └── main.go # Entrypoint
│ │ ├── internal/
│ │ │ ├── handler/ # gRPC handler (transport layer)
│ │ │ ├── service/ # Business logic
│ │ │ └── repository/ # Database layer
│ │ └── go.mod
│ ├── order-service/
│ │ └── ...
│ └── payment-service/
│ └── ...
├── pkg/ # Shared utilities
│ ├── interceptors/
│ ├── config/
│ └── logger/
└── Makefile
Design principle: Treat gRPC as a transport layer only. Keep your business logic in a separate service package that is independent of gRPC. This makes your code testable without needing a running gRPC server.
📝 Defining Protocol Buffers
The .proto File
Create proto/greeting.proto:
syntax = "proto3";
option go_package = "./greeting";
// Request message — each field has a type, name, and unique field number
message GreetingRequest {
string name = 1;
}
// Response message
message GreetingResponse {
string message = 1;
}
// Service definition — the compiler generates interfaces from this
service GreetingService {
rpc Greet(GreetingRequest) returns (GreetingResponse);
}
Key Concepts
Messages are containers for structured data. They are serialized into a compact binary format and sent over the wire. Each field is identified by its field number (not its name) in the binary encoding — so never change field numbers once your service is in production (this is how protobuf maintains backward compatibility).
Services define the RPC methods your server exposes. The service keyword tells the compiler to generate the corresponding Go interfaces.
go_package tells the compiler where to place the generated Go code, relative to the output directory.
Generating Go Code
Run this from the project root:
protoc --go_out=proto --go-grpc_out=proto proto/greeting.proto
| Flag | Purpose |
--go_out=proto | Generate message structs (greeting.pb.go) into the proto/ directory |
--go-grpc_out=proto | Generate gRPC service stubs (greeting_grpc.pb.go) into the proto/ directory |
This produces two files:
-
greeting.pb.go— Go structs forGreetingRequestandGreetingResponse, plus serialization methods -
greeting_grpc.pb.go— TheGreetingServiceServerinterface (you implement this) and theGreetingServiceClient(you call this)
🖥️ Implementing the gRPC Server
Create greeter-server/main.go:
package main
import (
"context"
"fmt"
"log"
"net"
proto "greeting-app/proto/greeting"
"google.golang.org/grpc"
)
// myGRPCServer implements the generated GreetingServiceServer interface.
// Embedding UnimplementedGreetingServiceServer ensures forward compatibility —
// if you add new RPCs to the .proto file, existing code won't break.
type myGRPCServer struct {
proto.UnimplementedGreetingServiceServer
}
// Greet is the actual business logic for the Greet RPC.
func (s *myGRPCServer) Greet(
ctx context.Context,
req *proto.GreetingRequest,
) (*proto.GreetingResponse, error) {
log.Printf("Received request to greet: %s", req.Name)
return &proto.GreetingResponse{
Message: "Hello " + req.Name,
}, nil
}
func main() {
port := ":8080"
log.Printf("Starting gRPC server on port %s", port)
// 1. Create a TCP listener
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
// 2. Create a new gRPC server instance
grpcServer := grpc.NewServer()
// 3. Register our service implementation with the server
proto.RegisterGreetingServiceServer(grpcServer, &myGRPCServer{})
// 4. Start serving requests
if err := grpcServer.Serve(lis); err != nil {
log.Fatalf("Failed to serve: %v", err)
}
}
What's Happening
-
We define a struct (
myGRPCServer) that embeds the auto-generatedUnimplementedGreetingServiceServer. This is a forward-compatibility pattern — if new RPCs are added to the.protofile, your code still compiles (the unimplemented methods return an "Unimplemented" error by default). -
We implement the
Greetmethod with our actual business logic. -
The
main()function creates a TCP listener, instantiates a gRPC server, registers the service, and starts serving.
📱 Implementing the gRPC Client
Create greeter-client/main.go:
package main
import (
"context"
"fmt"
"log"
"time"
proto "greeting-app/proto/greeting"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
fmt.Println("Starting gRPC client...")
// 1. Establish a connection to the gRPC server
conn, err := grpc.NewClient(
"localhost:8080",
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
log.Fatalf("Failed to connect: %v", err)
}
defer conn.Close()
// 2. Create a typed service client from the connection
client := proto.NewGreetingServiceClient(conn)
// 3. Set a timeout context
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 4. Call the Greet RPC
response, err := client.Greet(ctx, &proto.GreetingRequest{
Name: "John",
})
if err != nil {
log.Fatalf("Failed to greet: %v", err)
}
fmt.Printf("Response from server: %s\n", response.GetMessage())
}
Note:
grpc.Dialwithgrpc.WithInsecure()is deprecated. Usegrpc.NewClientwithgrpc.WithTransportCredentials(insecure.NewCredentials())in modern Go gRPC versions.
▶️ Running & Testing
Terminal 1 — Start the server:
go run greeter-server/main.go
# Output: Starting gRPC server on port :8080
Terminal 2 — Run the client:
go run greeter-client/main.go
# Output:
# Starting gRPC client...
# Response from server: Hello John
Server terminal should now also show:
Received request to greet: John
If you get import errors, run go mod tidy to resolve dependencies.
🔄 gRPC Streaming Patterns
gRPC supports four communication patterns, not just simple request-response:
1. Unary RPC (Request → Response)
rpc Greet(GreetingRequest) returns (GreetingResponse);
Standard single request, single response. Like a normal function call.
2. Server Streaming (Request → Stream of Responses)
rpc ListGreetings(ListRequest) returns (stream GreetingResponse);
Client sends one request, server sends back multiple responses over time. Good for fetching large datasets, live feeds, or paginated results.
// Server implementation
func (s *myServer) ListGreetings(
req *proto.ListRequest,
stream proto.GreetingService_ListGreetingsServer,
) error {
names := []string{"Alice", "Bob", "Charlie"}
for _, name := range names {
if err := stream.Send(&proto.GreetingResponse{
Message: "Hello " + name,
}); err != nil {
return err
}
}
return nil
}
3. Client Streaming (Stream of Requests → Response)
rpc RecordGreetings(stream GreetingRequest) returns (GreetingSummary);
Client sends multiple messages, server sends back a single summary. Good for uploading data in chunks or batch operations.
4. Bidirectional Streaming (Stream ↔ Stream)
rpc ChatGreetings(stream GreetingRequest) returns (stream GreetingResponse);
Both client and server send streams independently. Good for real-time chat, collaborative editing, or live dashboards.
🛡️ Interceptors (Middleware)
Interceptors are gRPC's equivalent of HTTP middleware. They let you inject logic (logging, auth, rate limiting, tracing) before and after RPC execution without touching your business logic. There are four types (two sides × two modes):
| Unary | Streaming | |
| Server | UnaryServerInterceptor | StreamServerInterceptor |
| Client | UnaryClientInterceptor | StreamClientInterceptor |
Logging Interceptor Example
func loggingInterceptor(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
start := time.Now()
// Call the actual RPC handler
resp, err := handler(ctx, req)
// Log after the call completes
log.Printf(
"method=%s duration=%s error=%v",
info.FullMethod,
time.Since(start),
err,
)
return resp, err
}
Auth Interceptor Example
func authInterceptor(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
// Extract metadata (gRPC's equivalent of HTTP headers)
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.Unauthenticated, "no metadata")
}
tokens := md.Get("authorization")
if len(tokens) == 0 || !isValidToken(tokens[0]) {
return nil, status.Error(codes.Unauthenticated, "invalid token")
}
return handler(ctx, req)
}
Chaining Multiple Interceptors
grpcServer := grpc.NewServer(
grpc.ChainUnaryInterceptor(
loggingInterceptor,
authInterceptor,
rateLimitInterceptor,
),
grpc.ChainStreamInterceptor(
streamLoggingInterceptor,
streamAuthInterceptor,
),
)
Interceptors execute left-to-right: logging → auth → rate limiting.
go-grpc-middleware (Community Library)
The go-grpc-middleware package provides production-ready interceptors:
| Interceptor | Purpose |
auth | Custom authentication logic via AuthFunc |
recovery | Catches panics and converts them to gRPC errors |
logging | Structured logging with zap, logrus, or slog |
prometheus | Client and server metrics |
retry | Automatic client-side retries with backoff |
validator | Auto-validates incoming messages from protobuf definitions |
ratelimit | Controls request rates |
selector | Applies interceptors only to specific RPC methods |
go get github.com/grpc-ecosystem/go-grpc-middleware/v2
⚠️ Error Handling
gRPC uses standardized status codes (similar to HTTP status codes, but purpose-built for RPC). Always return proper status codes instead of generic errors.
Common gRPC Status Codes
| Code | Meaning | When to Use |
OK | Success | Everything worked |
InvalidArgument | Bad input | Missing or malformed fields |
NotFound | Resource missing | User, record, etc. not found |
AlreadyExists | Duplicate | Unique constraint violation |
PermissionDenied | Forbidden | Authenticated but not authorized |
Unauthenticated | No/bad credentials | Missing or invalid token |
Internal | Server error | Unexpected failures |
Unavailable | Service down | Temporary — safe to retry |
DeadlineExceeded | Timeout | Client's deadline was reached |
ResourceExhausted | Rate limited | Too many requests |
Returning Errors in Go
import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func (s *myServer) Greet(
ctx context.Context,
req *proto.GreetingRequest,
) (*proto.GreetingResponse, error) {
if req.Name == "" {
return nil, status.Error(
codes.InvalidArgument,
"name is required",
)
}
// For richer error details:
// st, _ := status.New(codes.NotFound, "user not found").
// WithDetails(&errdetails.BadRequest{...})
// return nil, st.Err()
return &proto.GreetingResponse{
Message: "Hello " + req.Name,
}, nil
}
Handling Errors on the Client
response, err := client.Greet(ctx, request)
if err != nil {
st, ok := status.FromError(err)
if ok {
switch st.Code() {
case codes.InvalidArgument:
log.Printf("Bad request: %s", st.Message())
case codes.Unavailable:
log.Println("Server unavailable, retrying...")
// implement retry logic
default:
log.Printf("RPC error: code=%s msg=%s", st.Code(), st.Message())
}
}
return
}
🔒 TLS & Security
Never run gRPC without TLS in production.
Server with TLS
import "google.golang.org/grpc/credentials"
creds, err := credentials.NewServerTLSFromFile("server.crt", "server.key")
if err != nil {
log.Fatalf("Failed to load TLS: %v", err)
}
grpcServer := grpc.NewServer(grpc.Creds(creds))
Client with TLS
creds, err := credentials.NewClientTLSFromFile("ca.crt", "")
if err != nil {
log.Fatalf("Failed to load TLS: %v", err)
}
conn, err := grpc.NewClient(
"myservice.example.com:443",
grpc.WithTransportCredentials(creds),
)
🩺 Health Checks & Graceful Shutdown
gRPC Health Check Protocol
gRPC has a standardized health check service. Use it so load balancers and orchestrators (Kubernetes, ECS) can probe your service.
import "google.golang.org/grpc/health"
import healthpb "google.golang.org/grpc/health/grpc_health_v1"
grpcServer := grpc.NewServer()
healthServer := health.NewServer()
healthpb.RegisterHealthServer(grpcServer, healthServer)
// Set service status
healthServer.SetServingStatus("myservice", healthpb.HealthCheckResponse_SERVING)
Graceful Shutdown
func main() {
grpcServer := grpc.NewServer()
// ... register services ...
// Handle OS signals
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
go func() {
if err := grpcServer.Serve(lis); err != nil {
log.Fatalf("Failed to serve: %v", err)
}
}()
<-quit
log.Println("Shutting down gracefully...")
grpcServer.GracefulStop() // Finishes in-flight RPCs before stopping
log.Println("Server stopped")
}
📊 Production Checklist
| Area | What to Implement |
| Observability | Structured logging (zap/slog), Prometheus metrics, OpenTelemetry tracing |
| Security | mTLS between services, token-based auth interceptor, input validation |
| Resilience | Client-side retries with backoff, circuit breakers, deadline propagation |
| Health | gRPC health check protocol, Kubernetes readiness/liveness probes |
| Versioning | Never reuse or change protobuf field numbers; add new fields, deprecate old |
| CI/CD | Lint .proto files with buf lint, detect breaking changes with buf breaking |
| Deployment | Containerize with Docker, deploy on Kubernetes/ECS, use service mesh (Istio/Linkerd) |
| Testing | Unit test business logic separately, use bufconn for in-process gRPC integration tests |
🧪 Testing with bufconn
bufconn lets you test gRPC services in-process without opening a real TCP port:
import (
"google.golang.org/grpc/test/bufconn"
)
const bufSize = 1024 * 1024
var lis *bufconn.Listener
func init() {
lis = bufconn.Listen(bufSize)
s := grpc.NewServer()
proto.RegisterGreetingServiceServer(s, &myGRPCServer{})
go func() {
if err := s.Serve(lis); err != nil {
log.Fatal(err)
}
}()
}
func bufDialer(context.Context, string) (net.Conn, error) {
return lis.Dial()
}
func TestGreet(t *testing.T) {
ctx := context.Background()
conn, err := grpc.NewClient(
"passthrough:///bufnet",
grpc.WithContextDialer(bufDialer),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
t.Fatalf("Failed to dial: %v", err)
}
defer conn.Close()
client := proto.NewGreetingServiceClient(conn)
resp, err := client.Greet(ctx, &proto.GreetingRequest{Name: "Test"})
if err != nil {
t.Fatalf("Greet failed: %v", err)
}
if resp.Message != "Hello Test" {
t.Errorf("unexpected response: %s", resp.Message)
}
}
🌐 Exposing gRPC as REST (gRPC-Gateway)
For browser clients or public APIs, use gRPC-Gateway to auto-generate a REST reverse proxy from your .proto definitions:
import "google/api/annotations.proto";
service GreetingService {
rpc Greet(GreetingRequest) returns (GreetingResponse) {
option (google.api.http) = {
post: "/v1/greet"
body: "*"
};
}
}
This generates a REST endpoint POST /v1/greet that translates JSON to protobuf, forwards to your gRPC server, and translates the response back to JSON.
🧰 Useful Tools
| Tool | Purpose |
| Buf | Lint, format, and detect breaking changes in .proto files |
| grpcurl | curl for gRPC — test services from the CLI |
| Evans | Interactive gRPC client (REPL) |
| gRPC-Gateway | Auto-generate REST proxy from .proto |
| go-grpc-middleware | Production interceptors (auth, logging, metrics, retry) |
| Postman | gRPC support for API testing |
Quick CLI Testing with grpcurl
# List available services (requires reflection enabled)
grpcurl -plaintext localhost:8080 list
# Call an RPC
grpcurl -plaintext -d '{"name": "John"}' localhost:8080 GreetingService/Greet
Enable server reflection so tools can discover your services:
import "google.golang.org/grpc/reflection"
grpcServer := grpc.NewServer()
proto.RegisterGreetingServiceServer(grpcServer, &myGRPCServer{})
reflection.Register(grpcServer) // Enable reflection
📚 References
Last updated: March 2026