Microservices & gRPC
Go is the language of choice for building microservices due to its simplicity, performance, and excellent concurrency support. Kubernetes, Docker, Istio, etcd, and Prometheus are all written in Go — the infrastructure that runs microservices is itself built with Go, which tells you something about its fitness for this domain. This chapter covers gRPC, Protocol Buffers, and essential microservices patterns.gRPC Fundamentals
gRPC is a high-performance RPC framework that uses HTTP/2 and Protocol Buffers. Think of gRPC as a phone call between services: both sides agree on the language (protobuf schema) before the conversation starts, the connection is efficient (HTTP/2 multiplexing — multiple calls share one connection), and both sides can talk simultaneously (bidirectional streaming). REST, by comparison, is more like sending letters: each message is self-contained (JSON), the format is human-readable but verbose, and there is no built-in concept of an ongoing conversation.Why gRPC?
Protocol Buffers
Protocol Buffers (protobuf) is Google’s language-neutral serialization format.Generating Go Code
gRPC Server
Implementing the Service
gRPC Client
gRPC Interceptors (Middleware)
Server Interceptors
Client Interceptors
Service Discovery
Using Consul
Load Balancing with gRPC
Health Checks
Circuit Breaker
Event-Driven Architecture
Publishing Events
Consuming Events
Distributed Tracing
Interview Questions
What's the difference between gRPC and REST?
What's the difference between gRPC and REST?
- Protocol: gRPC uses HTTP/2, REST typically HTTP/1.1
- Payload: gRPC uses binary Protobuf, REST uses text (JSON/XML)
- Streaming: gRPC has native bidirectional streaming
- Type Safety: gRPC has strong contracts, REST relies on documentation
- Performance: gRPC is faster due to binary serialization and HTTP/2
What are the four types of gRPC methods?
What are the four types of gRPC methods?
- Unary: Single request, single response
- Server streaming: Single request, stream of responses
- Client streaming: Stream of requests, single response
- Bidirectional streaming: Stream in both directions
How do you handle errors in gRPC?
How do you handle errors in gRPC?
Use
google.golang.org/grpc/status package with standard codes:codes.NotFoundfor missing resourcescodes.InvalidArgumentfor bad inputcodes.Unauthenticatedfor auth failurescodes.PermissionDeniedfor authorization failurescodes.Internalfor server errors
What patterns do you use for service-to-service communication?
What patterns do you use for service-to-service communication?
- Circuit Breaker: Prevent cascading failures
- Retry with backoff: Handle transient failures
- Timeout: Prevent hanging requests
- Load balancing: Distribute traffic
- Service discovery: Dynamic endpoint resolution
- Health checks: Monitor service availability
Summary
Interview Deep-Dive
How do gRPC interceptors work in Go? Compare them to HTTP middleware and explain how you would implement logging, authentication, and error recovery.
How do gRPC interceptors work in Go? Compare them to HTTP middleware and explain how you would implement logging, authentication, and error recovery.
Strong Answer:
- gRPC interceptors are the gRPC equivalent of HTTP middleware. There are two types: unary interceptors (for request-response RPCs) and stream interceptors (for streaming RPCs). Each interceptor has access to the request, the server info (method name, service), and calls the next handler in the chain.
- The signature for a unary server interceptor is:
func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error). You can inspect/modify the request before callinghandler(ctx, req), and inspect/modify the response/error after. - For logging: record the start time, call the handler, then log the method name, duration, and error status. The
info.FullMethodgives you the service and method name (like/user.UserService/GetUser). - For authentication: extract the token from gRPC metadata (
metadata.FromIncomingContext(ctx)), validate it, and inject claims into the context withcontext.WithValue. Returnstatus.Error(codes.Unauthenticated, ...)if validation fails. This is analogous to reading theAuthorizationheader in HTTP middleware. - For error recovery: wrap the handler call in a
defer func() { if r := recover(); r != nil { ... } }()to catch panics and convert them tostatus.Error(codes.Internal, ...)instead of crashing the server. - Key difference from HTTP middleware: gRPC interceptors chain with
grpc.ChainUnaryInterceptor(), and gRPC uses structured error codes (codes.NotFound,codes.PermissionDenied) instead of HTTP status codes. The error codes are richer and more standardized than HTTP status codes for RPC semantics.
Your microservice makes calls to three downstream services. One of them starts returning errors. How do you prevent this from cascading and taking down your service?
Your microservice makes calls to three downstream services. One of them starts returning errors. How do you prevent this from cascading and taking down your service?
Strong Answer:
- Three layers of defense: timeouts, circuit breakers, and bulkheads.
- Timeouts: every downstream call must have a context timeout. If service C is slow, the request to C times out after (say) 2 seconds instead of hanging indefinitely. Without timeouts, your goroutines pile up waiting for the slow service, exhausting memory and goroutine capacity.
- Circuit breaker: after N consecutive failures to service C (or a failure rate threshold), the circuit opens and subsequent calls fail immediately without even attempting the request. This gives service C time to recover and prevents your service from wasting resources on requests that will fail. After a timeout period, the circuit moves to half-open and probes C with a single request. If it succeeds, the circuit closes.
- Bulkheads: isolate the failure domain. If service C’s connection pool is separate from services A and B, exhausting C’s pool does not affect A and B. In Go, this means using separate HTTP clients (with their own connection pools) or separate goroutine pools for each downstream service.
- Additionally: implement graceful degradation. If service C is the recommendation engine and it is down, return a static default recommendation set instead of failing the entire request. The user gets a slightly worse experience but the service stays up.
- In Go specifically: use
errgroup.WithContextso that if one downstream call fails, the context is cancelled and the other concurrent calls are aborted. Combine withcontext.WithTimeoutper call, andsony/gobreakerfor circuit breaking.