171 lines
7.5 KiB
Markdown
171 lines
7.5 KiB
Markdown
# Architecture Patterns
|
|
|
|
## Purpose
|
|
Select the right architectural style for your system's requirements. No pattern is universally best — each encodes trade-offs between complexity, scalability, team autonomy, and operational cost.
|
|
|
|
## Pattern Overview
|
|
|
|
| Pattern | Best For | Team Size | Complexity |
|
|
|---------|----------|-----------|------------|
|
|
| Modular Monolith | Most new projects, small-medium teams | 1-4 teams | Low-Medium |
|
|
| Microservices | Large orgs, independent deployment needs | 5+ teams | High |
|
|
| Serverless | Event-driven workloads, variable traffic | Any | Medium |
|
|
| Event-Driven | Async workflows, loose coupling, audit trails | 2+ teams | Medium-High |
|
|
| CQRS | Read-heavy with complex queries, separate scaling needs | 2+ teams | High |
|
|
| Hexagonal | Testability, swappable infrastructure, long-lived systems | Any | Medium |
|
|
|
|
## Modular Monolith
|
|
|
|
### Description
|
|
A single deployable unit with well-defined internal module boundaries. Modules communicate through explicit interfaces, not shared database tables.
|
|
|
|
### When to Use
|
|
- Starting a new product (default choice)
|
|
- Team is small (fewer than 20 developers)
|
|
- Domain boundaries are not yet clear
|
|
- Deployment simplicity is valued
|
|
|
|
### Key Rules
|
|
- Each module owns its data (separate schemas or schema prefixes)
|
|
- Modules communicate via defined interfaces (not direct database queries across modules)
|
|
- Module dependencies are acyclic and explicitly declared
|
|
- Extract to microservices later when boundaries are proven
|
|
|
|
### Trade-offs
|
|
- (+) Simple deployment, debugging, and testing
|
|
- (+) Refactoring across modules is straightforward
|
|
- (-) Scaling is all-or-nothing (cannot scale one module independently)
|
|
- (-) Technology choices are shared across all modules
|
|
|
|
## Microservices
|
|
|
|
### Description
|
|
Independently deployable services, each owning a bounded context with its own data store.
|
|
|
|
### When to Use
|
|
- Multiple teams need to deploy independently
|
|
- Different parts of the system have very different scaling needs
|
|
- Polyglot technology requirements
|
|
- Organization has mature DevOps and observability practices
|
|
|
|
### Key Rules
|
|
- Each service owns its database — no shared data stores
|
|
- Services communicate via APIs or events, never direct database access
|
|
- Design for failure: every remote call can fail
|
|
- Deploy independently with backward-compatible API changes
|
|
|
|
### Trade-offs
|
|
- (+) Independent deployment, scaling, and technology choice
|
|
- (+) Team autonomy and clear ownership boundaries
|
|
- (-) Distributed system complexity (network failures, data consistency, debugging)
|
|
- (-) Operational overhead (monitoring, deployment pipelines per service)
|
|
- (-) Integration testing is difficult
|
|
|
|
## Serverless
|
|
|
|
### Description
|
|
Application logic runs in ephemeral, event-triggered functions managed by the cloud provider. No server provisioning or management.
|
|
|
|
### When to Use
|
|
- Event-driven workloads (file processing, webhooks, scheduled tasks)
|
|
- Highly variable traffic with long idle periods
|
|
- Rapid prototyping with minimal infrastructure
|
|
- Cost optimization for low or bursty traffic
|
|
|
|
### Key Rules
|
|
- Functions should be stateless — use external stores for state
|
|
- Keep cold start impact low (small bundles, provisioned concurrency for latency-sensitive paths)
|
|
- Design for idempotency — events may be delivered more than once
|
|
- Set concurrency limits to protect downstream systems
|
|
|
|
### Trade-offs
|
|
- (+) Zero infrastructure management, pay-per-use pricing
|
|
- (+) Automatic scaling to zero and to peak
|
|
- (-) Cold start latency (problematic for synchronous user-facing requests)
|
|
- (-) Vendor lock-in to cloud provider's function runtime and event sources
|
|
- (-) Debugging distributed function chains is difficult
|
|
- (-) Execution time limits (15 minutes on AWS Lambda)
|
|
|
|
## Event-Driven Architecture
|
|
|
|
### Description
|
|
Components communicate by producing and consuming events through a message broker. Producers do not know about consumers.
|
|
|
|
### When to Use
|
|
- Loose coupling between components is essential
|
|
- Workflows are asynchronous (order processing, notifications, data pipelines)
|
|
- Audit trails and event replay are needed
|
|
- Multiple consumers need to react to the same event
|
|
|
|
### Key Patterns
|
|
- **Event Notification**: Signal that something happened; consumer fetches details
|
|
- **Event-Carried State Transfer**: Event contains all data the consumer needs
|
|
- **Event Sourcing**: Store the sequence of events as the source of truth (not current state)
|
|
|
|
### Trade-offs
|
|
- (+) Loose coupling, independent scalability, natural audit trail
|
|
- (-) Eventual consistency (not all components see changes simultaneously)
|
|
- (-) Event ordering and deduplication complexity
|
|
- (-) Debugging event flows requires distributed tracing
|
|
|
|
## CQRS (Command Query Responsibility Segregation)
|
|
|
|
### Description
|
|
Separate the write model (commands that change state) from the read model (queries that return data). Each can use different data stores and schemas optimized for their purpose.
|
|
|
|
### When to Use
|
|
- Read and write patterns are very different (read-heavy with complex projections)
|
|
- Read and write sides need to scale independently
|
|
- Domain model is complex and queries require flattened/denormalized views
|
|
|
|
### Key Rules
|
|
- Commands validate business rules and write to the write store
|
|
- Events propagate changes to the read store (eventually consistent)
|
|
- Read models are disposable — they can be rebuilt from the event stream
|
|
- Accept eventual consistency between write and read sides
|
|
|
|
### Trade-offs
|
|
- (+) Optimized read and write performance independently
|
|
- (+) Read models tailored to specific UI needs without compromising domain model
|
|
- (-) Increased complexity (two models, synchronization, eventual consistency)
|
|
- (-) Overkill for simple CRUD applications
|
|
|
|
## Hexagonal Architecture (Ports and Adapters)
|
|
|
|
### Description
|
|
Business logic at the center, surrounded by ports (interfaces) and adapters (implementations). External systems connect through adapters; business logic never depends on infrastructure.
|
|
|
|
### Structure
|
|
```
|
|
Adapters (Infrastructure)
|
|
└── Ports (Interfaces)
|
|
└── Domain (Business Logic) ← depends on nothing external
|
|
```
|
|
|
|
### When to Use
|
|
- Business logic is complex and must be testable in isolation
|
|
- Infrastructure may change (swap database, replace message broker, change cloud provider)
|
|
- Long-lived systems where technology choices will evolve
|
|
|
|
### Key Rules
|
|
- Domain layer has zero dependencies on frameworks or infrastructure
|
|
- All external interactions go through port interfaces defined by the domain
|
|
- Adapters implement ports and translate between domain and external formats
|
|
- Tests can use in-memory adapters (no database, no network required)
|
|
|
|
## Migration Paths
|
|
|
|
| From | To | Approach |
|
|
|------|----|----------|
|
|
| Monolith | Modular Monolith | Identify module boundaries, enforce interface contracts, separate data |
|
|
| Modular Monolith | Microservices | Extract one module at a time (strangler fig pattern), starting with the least coupled |
|
|
| Monolith | Serverless | Extract event-driven workflows first (background jobs, file processing) |
|
|
| Microservices | Modular Monolith | Consolidate when services are too small, team boundaries have shifted, or operational cost exceeds benefit |
|
|
|
|
## Decision Framework
|
|
1. Start with a modular monolith unless you have proven reasons not to
|
|
2. Extract to microservices only when team autonomy or independent scaling demands it
|
|
3. Use serverless for event-driven workloads and glue code, not for core request-response APIs
|
|
4. Apply CQRS only when read and write models genuinely diverge
|
|
5. Use hexagonal architecture in the domain layer regardless of the outer architecture style
|