Layered Architecture
The most widely used pattern. Layers separate concerns hierarchically:
┌─────────────────────────────────────┐ │ Presentation Layer │ │ (UI, API endpoints, controllers) │ ├─────────────────────────────────────┤ │ Business Logic Layer │ │ (services, domain models, rules) │ ├─────────────────────────────────────┤ │ Data Access Layer │ │ (repositories, DAOs, ORM) │ ├─────────────────────────────────────┤ │ Database │ │ (SQL, NoSQL, file storage) │ └─────────────────────────────────────┘
Pros: Simple, familiar, strong separation of concerns.
Cons: Can lead to monolithic designs, layers often leak.
Hexagonal Architecture (Ports & Adapters)
Business logic sits at the centre, isolated from external concerns. Adapters on the outside translate between the core and the outside world.
┌─────────────┐
│ Web UI │
│ (adapter) │
└──────┬──────┘
│
┌──────────────┐ ┌───────┴────────┐ ┌──────────────┐
│ PostgreSQL │◄───┤ Application │◄───┤ REST API │
│ (adapter) │ │ Core (hex) │ │ (adapter) │
└──────────────┘ └───────┬────────┘ └──────────────┘
│
┌──────┴──────┐
│ Message Q │
│ (adapter) │
└─────────────┘
Pros: High testability, framework independence, clear boundaries.
Cons: More initial structure, can feel abstract for simple systems.
Event-Driven Architecture
Components communicate asynchronously through events. An event bus or message broker (Kafka, RabbitMQ, SQS) routes events from producers to consumers.
┌──────────┐ order.created ┌──────────┐
│ Order │──────────────────►│ Kafka │
│ Service │ │ (broker) │
└──────────┘ └────┬─────┘
│
┌───────────────┼───────────────┐
│ │ │
┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐
│ Inventory │ │ Payment │ │Notificat'n│
│ Service │ │ Service │ │ Service │
└───────────┘ └───────────┘ └───────────┘
Pros: Loose coupling, excellent scalability, good for workflows.
Cons: Eventual consistency, harder to debug, schema management overhead.
CQRS (Command Query Responsibility Segregation)
Separates read and write models. Commands change state; queries read state. Often paired with Event Sourcing.
┌──────────────┐ ┌──────────────┐
│ Command │ │ Query │
│ Model │ │ Model │
│ (writes) │ │ (reads) │
└──────┬───────┘ └──────┬───────┘
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ Write DB │ │ Read DB │
│ (normalised) │◄────┤ (denormalis) │
└──────────────┘ └──────────────┘
▲
│
(sync/event)
Choosing a Pattern
| When | Pattern |
|---|---|
| Simple CRUD, small team | Layered |
| Complex domain, high testability needed | Hexagonal |
| High scale, async workflows | Event-Driven |
| Different read/write volume or shape | CQRS |
Design Principles
SOLID
- Single Responsibility
- Open/Closed
- Liskov Substitution
- Interface Segregation
- Dependency Inversion
Beyond SOLID
- KISS — prefer simple solutions over clever ones
- YAGNI — don’t build what you don’t need now
- Law of Demeter — talk only to your immediate neighbours
- Separation of Concerns — each module owns a distinct responsibility
Architecture Decision Records (ADRs)
Capture every significant decision using this template:
# Title: <short decision name>Context
What is the problem or motivation?
Decision
What did we decide?
Consequences
What trade-offs, risks, and benefits follow?
Store ADRs in version control alongside the code. Use a naming convention like NNNN-title.md (e.g., 0001-use-postgresql.md). Keep them short — if an ADR runs longer than one page, the decision scope may be too broad.