Chapter 20: Architectural Patterns
fsa architectural-patterns patterns
Status: Notes complete
Overview
Chapter 20 is new in the 2nd edition and addresses architectural patterns — reusable structural solutions to recurring design problems that operate at a finer granularity than architecture styles. Where a style defines the overall shape of a system, patterns provide specific structural mechanisms for solving common problems within or across styles. The chapter covers three categories of patterns: reuse patterns (how to share behavior without coupling), communication patterns (orchestration vs. choreography; CQRS), and infrastructure patterns (broker-domain separation). Understanding these patterns allows architects to compose solutions from proven building blocks rather than solving the same structural problems from scratch.
Patterns vs. Styles
The Core Distinction
| Dimension | Architecture Style | Architecture Pattern |
|---|---|---|
| Scope | Whole system — defines overall structure | Sub-system or cross-cutting concern |
| Granularity | Macro (system level) | Meso (component/service level) |
| Question answered | ”What does the whole system look like?" | "How do I solve this recurring structural problem here?” |
| Examples | Microservices, layered, event-driven | CQRS, Saga, Circuit Breaker, Sidecar, Broker-Domain |
| Relationship | Contains and constrains patterns | Used within or across styles |
A system uses one primary style (or occasionally a hybrid) but may employ many patterns simultaneously. The same pattern can appear in multiple styles — CQRS is useful in both microservices and modular monoliths.
Patterns vs. Design Patterns (GoF)
Gang of Four (GoF) design patterns (Observer, Factory, Strategy, etc.) operate at the code level — they solve local object-oriented design problems within a component.
Architectural patterns operate at the structural level — they solve problems about how components communicate, how data flows between services, and how responsibilities are divided across the system boundary.
| Level | Examples | Scope |
|---|---|---|
| Code (GoF) | Observer, Factory, Strategy, Decorator | Within a class or module |
| Architectural | CQRS, Sidecar, Saga, Broker-Domain | Across services or system components |
| Style | Microservices, layered, event-driven | Entire system |
Reuse Patterns
Separating Domain and Operational Coupling
The Problem
In any distributed system, services need operational capabilities that are not part of their business logic — logging, monitoring, metrics, authentication, authorization, rate limiting, health checking, and distributed tracing. Two naive approaches create problems:
- Embed operational code in each service: every service reimplements the same operational logic → code duplication, inconsistency, coupling operational concerns to domain logic
- Share a common operational library: creates a shared dependency → library version coordination across services, coupling through the dependency graph, monolithic-library risk
Domain Services vs. Operational Services
Domain services implement business logic — the “what the business does.” They should be pure expressions of business capability.
Operational services handle infrastructure concerns — the “how the system runs.” They should be invisible to domain logic.
The key principle: operational concerns must not leak into domain services. When a domain service contains logging framework code, auth token validation logic, or circuit breaker plumbing, it has become polluted by operational concerns — making it harder to test, change, and reason about.
Mechanisms for Operational Reuse Without Coupling
Shared Libraries:
- Packaging operational behavior (logging, metrics) into versioned libraries
- Pros: simple, no network overhead, synchronous
- Cons: creates build-time coupling; all services must upgrade together; library changes force redeployment of consuming services
Sidecar Pattern:
- A separate container/process runs alongside each service in the same deployment unit (e.g., same Kubernetes pod)
- The sidecar intercepts and augments network traffic — handling logging, auth, metrics, retries, and tracing without the service knowing
- Domain service is unaware of sidecar presence — zero operational coupling in domain code
- Pros: language-agnostic (sidecar can be a different language), consistent behavior, upgradeable independently
- Cons: network hop overhead, deployment complexity, debugging requires awareness of both components
Service Mesh:
- A service mesh is the platform-level implementation of the sidecar pattern at scale — every service in the cluster gets a sidecar proxy (e.g., Envoy in Istio), and a control plane manages configuration
- Provides service discovery, load balancing, mTLS, circuit breaking, retries, distributed tracing — all transparently
- Domain code contains zero infrastructure plumbing — the mesh handles it all
- Pros: complete separation of domain and operational concerns, centrally managed, powerful observability
- Cons: significant operational complexity (the mesh itself must be managed), debugging is harder, resource overhead per sidecar
Without Sidecar/Mesh:
┌──────────────────────────────┐
│ Order Service │
│ - Order domain logic │
│ - Auth token validation │ ← operational pollution
│ - Retry logic │ ← operational pollution
│ - Logging framework setup │ ← operational pollution
│ - Metrics publishing │ ← operational pollution
└──────────────────────────────┘
With Service Mesh:
┌────────────────┐ ┌──────────────────┐
│ Order Service │────▶│ Envoy Sidecar │
│ - Domain only │ │ - Auth │
└────────────────┘ │ - Retries │
│ - Metrics │
│ - mTLS │
└──────────────────┘
Communication Patterns
Orchestration vs. Choreography
These are not merely style characteristics (though they appear in discussions of microservices and event-driven architectures) — they are architectural patterns that can be applied wherever multi-service coordination is needed.
Orchestration Pattern
Definition: A central orchestrator component explicitly directs the actions of other services in a workflow. The orchestrator knows the entire workflow, calls each participant in sequence or parallel, handles the state machine, and aggregates results.
Workflow: Place Order
┌─────────────────────┐
Request ▶│ Order Orchestrator │
└──────┬──────────────┘
│ calls in sequence:
┌───────▼──────┐
│ InventoryService│ (reserve stock)
└───────┬───────┘
┌───────▼──────┐
│ PaymentService│ (charge card)
└───────┬───────┘
┌───────▼──────┐
│NotificationSvc│ (send email)
└──────────────┘
Characteristics:
- Tight control: orchestrator owns the workflow state machine
- Explicit flow: workflow steps are visible in orchestrator code — easy to understand and modify
- Single point of failure: if the orchestrator fails, the workflow stops
- Centralized error handling: compensating transactions and rollbacks are managed in one place
- Traceability: the full workflow state is in one component
Choreography Pattern
Definition: Services coordinate through events — each service listens for events it cares about, reacts to them, and emits new events. No central coordinator exists; the workflow emerges from the sum of reactions.
Workflow: Place Order (Choreography)
OrderPlaced event ──▶ InventoryService (listens)
│ emits: StockReserved
▼
PaymentService (listens to StockReserved)
│ emits: PaymentProcessed
▼
NotificationService (listens to PaymentProcessed)
│ emits: EmailSent
Characteristics:
- Loose coupling: each service only knows about its input events and output events, not the larger workflow
- High scalability: event processing can be parallelized and scaled independently per step
- Emergent workflow: the overall business process is not visible in any single place — it emerges from the event graph
- Difficult tracing: understanding the full workflow requires reconstructing it from event streams
- Complex error handling: distributed rollbacks require compensating events (Saga pattern) rather than simple function calls
Decision Matrix: Orchestration vs. Choreography
| Factor | Prefer Orchestration | Prefer Choreography |
|---|---|---|
| Workflow complexity | Complex multi-step, conditional logic | Simple linear reactive chains |
| Error handling | Complex rollbacks/compensation required | Idempotent operations, retries sufficient |
| Traceability need | High — audit trail, debugging critical | Low — eventual consistency acceptable |
| Coupling tolerance | Can accept orchestrator coupling | Must minimize service coupling |
| Team size | Small team — visibility matters | Large team — independent service evolution |
| Scalability | Moderate throughput | High throughput, bursty |
| State management | Complex state machine | Stateless or event-sourced state |
Rule of thumb: start with orchestration for clarity and correctness; migrate to choreography when scalability or coupling elimination becomes necessary.
CQRS — Command Query Responsibility Segregation
Definition
CQRS separates the write model (commands — mutate state) from the read model (queries — return data) into distinct components, often distinct services or at least distinct code paths.
Traditional (CRUD): CQRS:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Single Model │ │ Write Model │ │ Read Model │
│ Read + Write │ │ (Commands) │ │ (Queries) │
│ Same DB │ │ Source DB │ │ Read Store │
└──────────────┘ └──────┬───────┘ └──────────────┘
│ events/sync ▲
└───────────────────┘
Why CQRS Helps
- Read and write workloads differ dramatically: writes require consistency guarantees and complex validation; reads require fast, denormalized data optimized for query patterns. A single model cannot be simultaneously optimized for both.
- Independent scaling: the read side can be scaled out independently of the write side — add read replicas without affecting write throughput.
- Read model freedom: the read store can be a completely different technology (e.g., Elasticsearch for full-text search, Redis for caching, graph DB for relationship queries) optimized for the query pattern.
- Event sourcing compatibility: the write model becomes an event log; the read model is a projection of events — natural fit.
Event Sourcing as CQRS Complement
Event sourcing stores the history of changes as an ordered log of events (not the current state). The current state is derived by replaying the event log.
Combined with CQRS:
- Write side: append events to the event log (commands produce events)
- Read side: maintain projections (materialized views) built from the event log
Benefits of the combination:
- Complete audit trail — every state change is captured
- Read models can be rebuilt at any time by replaying events
- New read models can be created retroactively without changing write behavior
- Time travel — system state at any past moment is reconstructable
CQRS Trade-offs
| Benefit | Cost |
|---|---|
| Read and write models independently optimized | Eventual consistency — read model may lag behind write model |
| Independent scaling of read vs. write | Increased complexity — two models, synchronization mechanism |
| Read model technology freedom | Developer learning curve |
| Natural audit trail (with event sourcing) | Query complexity — reads cannot query the same DB as writes |
| Supports temporal queries | Synchronization failure handling — what if read model update fails? |
When to Use CQRS
Use CQRS when:
- Read and write workloads are asymmetric in scale (e.g., 1000:1 reads to writes)
- Multiple different query patterns must be optimized (e.g., full-text, geo, aggregated reporting)
- An audit trail is a business requirement
- Event sourcing is desired
- The domain has complex write-side business rules (Domain-Driven Design aggregates)
Avoid CQRS when:
- CRUD operations are simple and symmetric
- The team is small and consistency of the two models adds more confusion than value
- Eventual consistency of the read model is unacceptable to the business (e.g., financial balances that must be immediately consistent)
Infrastructure Patterns
Broker-Domain Pattern
The Problem
In event-driven and message-based architectures, services often couple their domain logic to the messaging infrastructure — they contain broker-specific code (e.g., Kafka consumer APIs, AMQP client code, queue connection management) directly in their business logic components. This creates:
- Infrastructure leakage into domain: domain code knows about topics, offsets, consumer groups, acknowledgment semantics
- Tight coupling to broker technology: changing from Kafka to RabbitMQ requires changes throughout domain code
- Difficult testing: domain logic cannot be tested without a real or mocked broker
- Mixed responsibilities: a single component both handles message routing and executes business logic
The Pattern
The Broker-Domain Pattern separates two distinct responsibilities into separate components:
Broker component (infrastructure concern):
- Connects to the message broker (Kafka, RabbitMQ, SQS, etc.)
- Handles connection management, topic subscriptions, message deserialization
- Manages acknowledgment, retry, dead-letter queue behavior
- Translates broker-specific messages into domain-neutral DTOs/events
- Routes messages to the appropriate domain handler
Domain component (business concern):
- Receives domain-neutral events/commands from the broker component
- Executes business logic with no knowledge of messaging infrastructure
- Returns results to the broker component for publishing
- Contains no broker imports, no consumer group configuration, no acknowledgment logic
┌─────────────────────────────────────────────────────┐
│ Bid Processing Service │
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ Broker Component │──▶│ Domain Component │ │
│ │ │ │ │ │
│ │ - Kafka consumer │ │ - Validate bid │ │
│ │ - Deserialization │ │ - Apply bid rules │ │
│ │ - Ack management │ │ - Determine winner │ │
│ │ - Dead-letter queue │ │ - Emit domain event │ │
│ │ - Topic routing │ │ (no Kafka code) │ │
│ └─────────────────────┘ └─────────────────────┘ │
└─────────────────────────────────────────────────────┘
Benefits of Broker-Domain Separation
- Testability: domain component can be unit tested by injecting domain events directly — no broker required
- Technology independence: changing message broker requires rewriting only the broker component
- Clear responsibilities: infrastructure engineers own the broker component; domain engineers own the domain component — reduced cognitive load and coordination cost
- Evolvability: domain logic evolves independently of infrastructure plumbing
Key Takeaways
- Patterns vs. Styles: styles define the overall system shape; patterns are reusable structural solutions to specific recurring problems within a style — a system has one style but may use many patterns simultaneously.
- Architectural vs. GoF Patterns: GoF patterns solve code-level OO design problems; architectural patterns solve structural problems across services and system boundaries.
- Sidecar and Service Mesh: the sidecar pattern eliminates operational coupling from domain code by delegating auth, logging, retries, and tracing to a co-located proxy; service mesh scales this to the entire cluster via a control plane.
- Orchestration = Control, Choreography = Decoupling: orchestration provides explicit workflow control and traceability at the cost of coupling; choreography provides loose coupling and scalability at the cost of emergent, harder-to-trace workflows.
- CQRS Separates Read and Write Models: when read and write workloads are asymmetric, CQRS allows each model to be optimized and scaled independently — at the cost of eventual consistency and increased complexity.
- Event Sourcing Complements CQRS: storing state as an event log rather than current state enables audit trails, retroactive projections, and time travel — the write side appends events, read sides maintain projections.
- Broker-Domain Pattern Prevents Infrastructure Leakage: separating broker infrastructure code from domain logic makes domain components testable without a real broker and decouples business logic from messaging technology choices.
- Operational vs. Domain Coupling Is a Design Decision: operational concerns (logging, auth, monitoring) should not reside in domain services — shared libraries, sidecars, or service mesh are progressively more powerful mechanisms for separating them.
- No Pattern Is Always Right: CQRS adds complexity that is only justified when read/write asymmetry exists; choreography is only preferable to orchestration when coupling is a greater problem than traceability.
Related Resources
- ch09-architecture-styles-foundations — Styles vs. patterns foundational distinction
- ch19-choosing-architecture-style — How to select the style in which these patterns will be deployed
- ch11-modular-monolith-architecture — CQRS and broker-domain patterns applicable within modular monoliths
Last Updated: 2026-05-29