32. Architecture Patterns
Architecture Patterns
TL;DR
- Every pattern in this category — microservices, CQRS, Saga, circuit breakers, bulkheads — is a deliberate trade-off, not an inherently “correct” modern architecture to adopt by default.
- Strangler Fig de-risks monolith-to-services migrations by incrementally routing functionality behind a facade instead of a big-bang rewrite.
- CQRS separates read and write models at the cost of eventual consistency; Saga replaces distributed transactions with compensating actions at the cost of design complexity.
- Circuit breakers stop calls to a known-failing dependency; bulkheads isolate resource pools per dependency — complementary, not interchangeable.
- Any pattern adopted without a named cost alongside its named benefit hasn’t actually been evaluated.
Core Concepts
Microservices and event-driven architecture
Trade a simpler, single-deployable monolith for independently deployable, independently scalable services — at the real cost of distributed systems complexity: network calls where function calls used to be, eventual consistency where a single database transaction used to be, and a harder operational and debugging story. Worth it when team boundaries and independent deployment cadence justify it, not by default.
Strangler Fig
De-risks the migration from monolith to services by incrementally routing specific pieces of functionality from the old system to new services behind a facade, while the whole system stays working throughout. The alternative — a big-bang rewrite where nothing works until everything’s done — is a common failure mode that stalls real migrations for a year or more, leaving the system in a half-working, half-old, half-new state worse than either endpoint.
CQRS (Command Query Responsibility Segregation)
Separates the read model from the write model — valuable when read and write patterns are different enough (a write-heavy transactional core, a read-heavy denormalized reporting need) that a single shared model strains under both. The real cost, always: eventual consistency between the two models, and the synchronization complexity to keep the read model updated. Adopting CQRS without acknowledging that trade-off — reports sometimes a few seconds stale — leads to confused users asking why a just-completed action isn’t showing up yet.
Saga
Solves distributed transactions across services without a traditional two-phase-commit: each step in a multi-service transaction has a corresponding compensating action, and if a later step fails, earlier steps are reversed via their compensating actions in order. This gives eventual consistency across services at the cost of real design complexity — every step needs a correctly-implemented compensating action, and partial-failure states need to be genuinely handled, not hand-waved.
Hexagonal architecture (ports and adapters)
Isolates core business logic from external concerns (databases, APIs, messaging) behind defined interfaces — valuable for testability and for swapping infrastructure without touching business logic, at the cost of more upfront structure and indirection than a simpler, more directly-coupled design.
Serverless
An architectural pattern choice with its own trade-offs (operational simplicity vs. cold starts and execution limits), not just a deployment target.
Bulkhead and Circuit Breaker
The resilience patterns most directly relevant to a services architecture’s failure modes, solving genuinely different problems that are easy to conflate. A circuit breaker watches calls to a specific dependency, and after detecting repeated failures, “opens” — failing fast without even attempting the call, instead of every request hanging until a slow timeout. This protects the caller from wasting resources on a dependency known to be failing, and gives the downstream service room to recover without being hammered by retries. A bulkhead isolates resources — thread pools, connection pools — per dependency, so a slow or failing call to one dependency can’t exhaust the resources needed for calls to a different, healthy dependency. They’re complementary: a circuit breaker stops calls to a known-bad dependency; a bulkhead stops a bad dependency from starving calls to good ones even before the circuit breaker has tripped.
CQRS vs Saga vs Traditional Distributed Transaction
| Aspect | CQRS | Saga | Two-phase commit |
|---|---|---|---|
| Problem solved | Read/write model mismatch under one schema | Multi-service transaction without a shared DB transaction | Atomic commit across services |
| Consistency model | Eventual, between read and write models | Eventual, via compensating actions on failure | Strong, atomic |
| Real cost | Synchronization complexity, stale reads | Every step needs a correct compensating action | Tight coupling, poor availability, rarely practical across services |
| When it fits | Divergent read/write patterns at scale | Multi-service workflows needing rollback semantics | Rarely — avoided in distributed/microservice designs |
Common Pitfalls
- Pitfall: A big-bang microservices rewrite stalls in a half-migrated state. Why: Nothing works end-to-end until the entire rewrite is complete. Fix: Use Strangler Fig — route functionality incrementally behind a facade, keeping the system working throughout.
- Pitfall: CQRS or Saga adopted without designing for their real cost. Why: Eventual consistency and compensating-action complexity aren’t hand-waved details — they’re core design requirements. Fix: Explicitly design the eventual-consistency window and every compensating action, including partial-failure cases.
- Pitfall: No circuit breaker on a known-flaky dependency. Why: Every request waits for a slow timeout, tying up resources and risking cascading failure upstream. Fix: Add a circuit breaker that opens after repeated failures and fails fast.
- Pitfall: Circuit breakers present but no bulkheads. Why: A failing dependency’s exhausted connection pool still starves calls to unrelated, healthy dependencies sharing the same pool. Fix: Isolate resource pools (threads, connections) per dependency.
- Pitfall: Patterns adopted because they’re associated with sophisticated engineering. Why: No specific problem in the actual system justified the added complexity. Fix: Require an explicit statement of the problem solved and the cost incurred before adopting any pattern.
Interview Questions
- “A monolith-to-microservices migration stalled for a year in a half-working state. What would you have done differently?” — tests whether Strangler Fig is understood as the risk-reduction answer, not just a term.
- “Explain the difference between a circuit breaker and a bulkhead with a concrete failure scenario where you’d want both.” — tests understanding of the distinct failure-propagation paths each addresses.
- “A team wants to adopt CQRS. What questions do you ask before agreeing it’s the right call?” — tests whether the candidate interrogates the actual read/write pattern mismatch and names the eventual-consistency cost explicitly.
- “A distributed transaction spans four microservices and step three fails after steps one and two succeeded. How do you handle rollback without a shared database transaction?” — tests understanding of the Saga pattern and compensating actions.