Skip to main content
Current1mo ago

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:

TechnologyRole
HTTP/2Multiplexed connections, header compression, server push
Protocol Buffers (protobuf)Binary serialization — compact, fast, language-neutral
Code generationAuto-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.

AspectREST (HTTP/JSON)gRPC (HTTP/2 + Protobuf)
SerializationJSON (text-based, verbose)Protobuf (binary, compact)
PerformanceBaseline~7–10× faster throughput
ContractOpenAPI/Swagger (optional).proto file (enforced)
StreamingWebsockets (separate layer)Native bidirectional streaming
Code generationLimitedFirst-class, multi-language
Browser supportNativeRequires gRPC-Web or gateway
HTTP versionHTTP/1.1HTTP/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

FlagPurpose
--go_out=protoGenerate message structs (greeting.pb.go) into the proto/ directory
--go-grpc_out=protoGenerate gRPC service stubs (greeting_grpc.pb.go) into the proto/ directory

This produces two files:

  • greeting.pb.go — Go structs for GreetingRequest and GreetingResponse, plus serialization methods

  • greeting_grpc.pb.go — The GreetingServiceServer interface (you implement this) and the GreetingServiceClient (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

  1. We define a struct (myGRPCServer) that embeds the auto-generated UnimplementedGreetingServiceServer. This is a forward-compatibility pattern — if new RPCs are added to the .proto file, your code still compiles (the unimplemented methods return an "Unimplemented" error by default).

  2. We implement the Greet method with our actual business logic.

  3. 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.Dial with grpc.WithInsecure() is deprecated. Use grpc.NewClient with grpc.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):

UnaryStreaming
ServerUnaryServerInterceptorStreamServerInterceptor
ClientUnaryClientInterceptorStreamClientInterceptor

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:

InterceptorPurpose
authCustom authentication logic via AuthFunc
recoveryCatches panics and converts them to gRPC errors
loggingStructured logging with zap, logrus, or slog
prometheusClient and server metrics
retryAutomatic client-side retries with backoff
validatorAuto-validates incoming messages from protobuf definitions
ratelimitControls request rates
selectorApplies 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

CodeMeaningWhen to Use
OKSuccessEverything worked
InvalidArgumentBad inputMissing or malformed fields
NotFoundResource missingUser, record, etc. not found
AlreadyExistsDuplicateUnique constraint violation
PermissionDeniedForbiddenAuthenticated but not authorized
UnauthenticatedNo/bad credentialsMissing or invalid token
InternalServer errorUnexpected failures
UnavailableService downTemporary — safe to retry
DeadlineExceededTimeoutClient's deadline was reached
ResourceExhaustedRate limitedToo 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

AreaWhat to Implement
ObservabilityStructured logging (zap/slog), Prometheus metrics, OpenTelemetry tracing
SecuritymTLS between services, token-based auth interceptor, input validation
ResilienceClient-side retries with backoff, circuit breakers, deadline propagation
HealthgRPC health check protocol, Kubernetes readiness/liveness probes
VersioningNever reuse or change protobuf field numbers; add new fields, deprecate old
CI/CDLint .proto files with buf lint, detect breaking changes with buf breaking
DeploymentContainerize with Docker, deploy on Kubernetes/ECS, use service mesh (Istio/Linkerd)
TestingUnit 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

ToolPurpose
BufLint, format, and detect breaking changes in .proto files
grpcurlcurl for gRPC — test services from the CLI
EvansInteractive gRPC client (REPL)
gRPC-GatewayAuto-generate REST proxy from .proto
go-grpc-middlewareProduction interceptors (auth, logging, metrics, retry)
PostmangRPC 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