Microservices architecture decomposes a monolith into independently deployable services, each owning a bounded domain. This enables independent scaling, technology heterogeneity, and faster deployments — but introduces distributed systems complexity: network failures, data consistency across services, and observability challenges. Understanding key microservices patterns (circuit breakers, bulkheads, saga, strangler fig) is essential for senior engineering roles.
Circuit Breaker Pattern
A circuit breaker prevents cascading failures: when a downstream service is failing, stop sending requests rather than queuing them and wasting threads. Three states: (1) Closed (normal operation): requests pass through. Track failures; if the failure rate exceeds the threshold (e.g., > 50% failures in a 10-second window), trip to Open. (2) Open (failing): immediately return an error to callers — don’t attempt the request. After a configurable timeout (30-60 seconds), transition to Half-Open. (3) Half-Open: allow a limited number of probe requests. If they succeed: reset to Closed. If they fail: return to Open. Implementation: Resilience4j (Java), Polly (.NET), Hystrix (deprecated). Configure per downstream dependency: the payment service circuit breaker is independent of the inventory service circuit breaker. Fallback behavior: when the circuit is Open, return a fallback value (cached response, default value) rather than an error if possible. A product page can show cached pricing when the pricing service is down — degraded but functional. Metrics: track circuit state transitions, call volume, and failure rates per circuit. Alert when a circuit trips.
Bulkhead Pattern
The bulkhead pattern isolates failures to prevent them from consuming all shared resources. Named after ship compartments (bulkheads) that prevent one flooded compartment from sinking the ship. Thread pool isolation: instead of a shared thread pool for all outgoing calls, allocate separate thread pools per downstream service. A slow payment service fills its 10-thread pool but cannot consume threads from the inventory service pool (which has its own 10 threads). If payment is slow, payment calls queue or fail — but inventory calls continue normally. Semaphore isolation: a simpler alternative to thread pools — limit concurrent calls to each downstream service using a semaphore. If the semaphore limit is reached, new calls are immediately rejected rather than waiting. Rate limiting per client: in a multi-tenant API, isolate tenant traffic so one tenant’s burst doesn’t starve others. Each tenant has its own rate limit bucket. Connection pool isolation: databases, HTTP connections, and Redis connections are shared resources. Size connection pools to match the expected concurrency per service. If the checkout service has 20 database connections and payment has 10, a surge in checkout traffic can’t exhaust payment’s connections.
Strangler Fig Pattern
The strangler fig pattern incrementally migrates a monolith to microservices without a big-bang rewrite. Named after a vine that gradually surrounds and replaces a host tree. Migration strategy: (1) Identify a bounded domain to extract (e.g., user authentication). (2) Implement the new microservice alongside the monolith — same data, separate code. (3) Route a percentage of traffic (5%, then 50%, then 100%) to the new service using a routing layer (API gateway or feature flag). (4) When the new service handles 100% of traffic, remove the monolith’s implementation. Repeat for each domain. The monolith continues operating throughout migration — no big-bang cutover, no downtime. The routing layer (API gateway or reverse proxy) is critical: it transparently forwards requests to either the monolith or the new service based on routing rules. Dual-write during migration: the new service and monolith share the same database initially. Gradually migrate the schema ownership to the new service. Anti-corruption layer: if the monolith’s domain model is messy, the new microservice implements a clean model and an ACL translates between them — preventing the old model’s problems from infecting the new service.
Saga Pattern for Distributed Transactions
When a business transaction spans multiple microservices (e-commerce checkout: inventory service, payment service, order service), traditional ACID transactions are impossible without distributed locking. The saga pattern decomposes the transaction into a sequence of local transactions with compensating actions for rollback. Choreography-based saga: each service publishes events when it completes its step; other services subscribe and react. Checkout saga: OrderService creates order (status: PENDING) and publishes OrderCreated. InventoryService consumes OrderCreated, reserves inventory, publishes InventoryReserved. PaymentService consumes InventoryReserved, charges card, publishes PaymentCompleted. OrderService consumes PaymentCompleted, marks order CONFIRMED. If payment fails: PaymentService publishes PaymentFailed. InventoryService consumes PaymentFailed and runs the compensating action (release reservation). Orchestration-based saga: a central saga orchestrator explicitly tells each service what to do and handles failures. Easier to understand the flow; the orchestrator is a complexity bottleneck. Idempotency: each saga step must be idempotent — if an event is delivered twice (at-least-once delivery), the duplicate must be safe to process. Use idempotency keys per saga step.
API Composition and CQRS
In a microservices architecture, data is distributed across services — a user profile dashboard needs data from the user service, order service, notification service, and subscription service. API composition: the API gateway (or a dedicated BFF — Backend for Frontend) fans out calls to all required services in parallel, merges the responses, and returns a single result. Handle partial failures gracefully — if the notification service is down, return the user profile with an empty notification list rather than failing the entire request. CQRS (Command Query Responsibility Segregation): separate the write model (commands that change state) from the read model (queries). Commands go to the authoritative service; queries read from denormalized views (projections) optimized for the specific query pattern. In a microservices context: when data changes in service A, it publishes an event; service B consumes the event and updates its local read-optimized copy. Service B can now answer queries about A’s data without calling A — removing the runtime dependency. Eventual consistency trade-off: B’s data may be slightly behind A’s. Acceptable for most reads; use synchronous API composition for reads that must be strongly consistent (payment balance).