Architectural Patterns
Design-time reference for structural patterns that govern how a system’s components are organized, coupled, and evolved. Covers decision criteria rather than just definitions — the goal is knowing when to apply each pattern, not just what it is.Monolith vs Microservices
Monolith
A single deployable unit containing all application logic. Not inherently bad — it is the correct starting point for most systems. Choose monolith when:- Team is small (< ~8 engineers working on the same codebase)
- Domain boundaries are not yet clear — premature decomposition creates the wrong services
- Operational overhead of distributed systems is not justified by the scale
- Latency between components matters (monolith avoids network hops)
- Deployment coupling: any change requires redeploying the whole system
- Scaling bottleneck: cannot scale high-traffic components independently
- Technology lock-in: entire system must use the same language/runtime
- Team coordination overhead grows superlinearly as engineers multiply
Microservices
A suite of independently deployable, small, modular services. Each owns its data, runs its own process, and communicates over a well-defined interface (REST, gRPC, message queue). Choose microservices when:- Teams need to deploy independently without coordinating with other teams
- Different components have different scaling requirements (read-heavy vs write-heavy)
- You need independent technology choices per service
- The domain is well-understood enough that service boundaries can be drawn without constant revision
- Distributed systems problems: network failures, latency, partial failures (see Distributed Systems)
- Data consistency: no cross-service transactions without Saga or 2PC
- Operational overhead: service discovery, observability, deployment pipelines per service
- Testing complexity: integration tests across service boundaries are harder
Decision heuristic
Start with a modular monolith. Extract a service only when a specific, concrete driver exists: independent deployment, independent scaling, team autonomy. Never decompose speculatively.Event-Driven Architecture
Services communicate by producing and consuming events rather than calling each other directly. Core components:- Event producer: emits events when state changes (e.g., “OrderPlaced”)
- Event broker: durable, ordered event log (Kafka, Kinesis, SQS)
- Event consumer: subscribes to events and reacts
- Loose temporal and spatial coupling — producer doesn’t know or wait for consumers
- Easy to add new consumers without changing producers
- Natural audit log when events are durable
- Enables fan-out: one event triggers multiple independent downstream processes
- Event schema evolution: consumers must handle old event shapes gracefully
- Ordering guarantees: most brokers guarantee order per partition/shard, not globally
- At-least-once delivery: consumers must be idempotent (see Distributed Systems)
- Observability: tracing request flows across async boundaries requires correlation IDs
CQRS (Command Query Responsibility Segregation)
Separates the model for writes (commands) from the model for reads (queries).When CQRS is warranted
CQRS adds operational complexity. It is justified when:- Read and write load patterns are significantly different (e.g., 100:1 read:write ratio)
- Read and write models have divergent schema needs (aggregated reports vs. transactional records)
- You need to scale reads and writes independently
- The domain is complex enough that a single CRUD model becomes a bottleneck for both teams and performance
CQRS + Event Sourcing on AWS (reference implementation)
The AWS prescriptive guidance pattern uses:- Lambda functions for command handlers (write operations: create, update, delete)
- Separate Lambda functions for query handlers (read operations: get, select)
- Separate DynamoDB tables for command DB and query DB
- DynamoDB Streams as the event sourcing mechanism to synchronize command DB changes to query DB
Event Sourcing
Instead of storing current state, store an append-only log of events that led to the current state. Current state is derived by replaying events. Benefits:- Full audit trail by default — every state change is recorded
- Temporal queries: reconstruct state at any past point in time
- Replay: reprocess events through a new projection to build a new query model
- Decoupling: downstream consumers derive their own views from the same event stream
- Avoids update conflicts — writers append, never overwrite
- Schema evolution: old events must be interpretable by new code (versioned event schemas)
- Eventually consistent query models — there is a lag between write and query DB sync
- Undo requires a compensating event, not a DELETE
- Learning curve: different from CRUD thinking
Hexagonal Architecture (Ports and Adapters)
The core application logic (domain) is isolated at the center. All external interactions (HTTP, DB, message queues, third-party APIs) connect through defined ports (interfaces) implemented by adapters (concrete implementations).Layered Architecture
The classic N-tier: Presentation → Application/Business Logic → Data Access → Database. Each layer only calls the layer directly below it. Dependencies flow in one direction. Tradeoffs:- Simple to understand and reason about
- Works well for CRUD applications with straightforward flows
- Can become an anti-pattern when strict layering forces data to traverse unnecessary abstractions (“anemic domain model”)
- Does not enforce dependency inversion — layers often end up tightly coupled despite the appearance of separation
Modular Monolith
An architectural middle ground: a single deployable unit (monolith) internally organized into strictly separated modules, each with its own bounded context, clear public API, and no direct access to another module’s internals or database tables.Vertical Slice Architecture
Organizes code around features (vertical slices through all layers) rather than technical layers (horizontal slices). Each feature owns its own handler, service, repository, and data model.Strangler Fig Pattern
A migration strategy for decomposing a monolith incrementally, without a big-bang rewrite.- Place a facade (reverse proxy or router) in front of the monolith
- Implement one feature as a new microservice
- Route that feature’s traffic to the new service through the facade
- Repeat — incrementally strangle the monolith until it no longer handles any traffic
Cross-references
- Distributed Systems — consistency, saga, idempotency — required reading before adopting microservices
- System Design Process — requirements clarification before choosing an architecture
- Software Design Principles — SOLID, single responsibility, dependency inversion — underpins hexagonal and layered patterns
- Agentic CI/CD — CI as external watchdog when agents are building or migrating systems