Command Palette

Search for a command to run...

Level 2 · 25 min

Microservices Patterns

Microservices patterns solve the distributed systems challenges introduced by service decomposition. Circuit breakers, API gateways, service discovery, and resilient communication patterns are the building blocks of production microservices architectures.

Service Communication

Synchronous (request-reply) communication: REST (HTTP/JSON) and gRPC (HTTP/2, binary Protocol Buffers). Simple, familiar, but creates temporal coupling — both services must be available simultaneously. If Service B is slow, Service A is slow. If Service B is down, Service A's requests fail. Asynchronous communication via message queues (Kafka, RabbitMQ) or events: Service A publishes a message and continues without waiting for a response. Service B processes when ready. Advantages: temporal decoupling (B can be down without affecting A), natural load buffering, easier retry on failure. Disadvantages: eventual consistency, complex error handling, harder to trace. The rule of thumb: use sync for user-facing real-time responses, async for background processing and cross-service state changes.

API Gateway and Circuit Breaker

API Gateway: a single entry point for all client traffic. Handles: request routing to downstream services, authentication/authorization (JWT validation), rate limiting, SSL termination, request/response transformation, API versioning. Without a gateway, clients must know all service URLs and handle auth separately. With a gateway, the complexity is centralized. Kong, AWS API Gateway, NGINX Plus, and Envoy are common implementations. Newman draws a critical distinction: service meshes and API gateways can share microservice-agnostic behavior (service discovery, logging, circuit breaking) without requiring new client libraries — but the moment business logic enters the gateway, it becomes a deployable bottleneck. Circuit Breaker (Hystrix, Resilience4j, Polly): wraps a remote call and tracks failure rate. States: Closed (normal operation), Open (circuit tripped — calls fail immediately, not forwarded to failing service), Half-Open (test calls allowed to check if service recovered). Benefits: prevents cascade failures — Newman's insight on bulkheads: 'If one component of a system fails, but that failure doesn't cascade, you can isolate the problem, and the rest of the system can carry on working. Service boundaries become your obvious bulkheads.' Without explicit circuit breakers, your P99 latency becomes the sum of all downstream P99 latencies in the call chain. — Sam Newman, Building Microservices (2nd ed.)

Service Discovery

In microservices, service instances start and stop dynamically (containers, autoscaling). Hardcoded service URLs break when instances change. Service discovery solves this. Client-side discovery: each service queries a service registry (Consul, Eureka) to find available instances and load-balances itself. Server-side discovery: a load balancer or API gateway queries the registry — services only talk to the gateway. Kubernetes provides built-in DNS-based service discovery: services are addressable by name within the cluster (http://orders-service:8080). Kubernetes also provides L4 (ClusterIP) and L7 (Ingress) load balancing. Service mesh (Istio, Linkerd): injects a sidecar proxy into each pod for traffic management, mTLS, circuit breaking, and observability — all without application code changes.

Key Takeaways

  • Use sync communication (REST/gRPC) for user-facing real-time operations. Use async (events/queues) for background processing, cross-service state changes, and any operation that doesn't need an immediate response.
  • Circuit breakers prevent cascade failures. Without them, a slow downstream service can exhaust thread pools in all upstream services via the network timeout chain.
  • In Kubernetes, service discovery is built in via DNS. For non-Kubernetes deployments, Consul or Eureka with client-side load balancing is the standard pattern.

Code example

// Circuit breaker with Resilience4j (Java)\nCircuitBreakerConfig config = CircuitBreakerConfig.custom()\n  .failureRateThreshold(50)        // Open if 50% of calls fail\n  .waitDurationInOpenState(Duration.ofSeconds(30))\n  .slidingWindowSize(10)\n  .build();\n\nCircuitBreaker cb = CircuitBreakerRegistry.of(config).circuitBreaker('payment');\nSupplier<String> supplier = CircuitBreaker.decorateSupplier(cb, () -> paymentService.charge());\n// Returns fallback if circuit is open