Quick Navigation
- Start here — What patterns are
- Layered architecture
- Microservices
- Event-driven architecture
- CQRS and event sourcing
- Hexagonal architecture
- Serverless
- Choosing a pattern
- Putting it all together
- Cheat sheet
Before we start — the one thing to hold onto
Architecture patterns aren't trends to follow. They're proven structures with known trade-offs. I've seen teams pick microservices because it was trending, then spend eighteen months fighting distributed systems complexity for a product that three people used.
Every pattern solves a specific problem and sacrifices something else. The architect's job isn't to pick the "best" pattern — it's to match the pattern's strengths to the system's context and accept its costs.
Most systems combine multiple patterns at different layers. Keep that in mind.
1. What patterns are — And why they matter
Every system faces structural decisions — how to organise components, how to manage dependencies, how to handle change. Starting from scratch each time is slow and error-prone.
Patterns capture the collective experience of architects who have solved similar problems before. They tell you: "When you face this kind of problem, this structure tends to work — but watch out for this trade-off."
What a pattern gives you:
A structure — how to organise components and their relationships. A context — when this pattern is appropriate. A trade-off — what you gain and what you sacrifice. Known failure modes — how this pattern goes wrong when misapplied.
flowchart TD
PROB["🏗️ Recurring Problem\nHow to organise components\nfor scalability, testability, change"]
PROB --> PAT["📐 Pattern\nProven structure with\nknown trade-offs"]
PAT --> CTX["📋 Context\nWhen to use it"]
PAT --> WIN["✅ What you gain"]
PAT --> COST["❌ What you sacrifice"]
PAT --> FAIL["⚠️ Known failure modes"]
Here's the part that catches people out: a pattern isn't a solution. It's a starting point that you adapt to your context, with known risks you choose to accept.
Patterns are collective experience captured as structure — not rules to follow, but wisdom to apply.
Try it yourself — The pattern audit
What patterns is your system actually using right now? Be honest — not what you planned, what you're running:
| Pattern | Evidence | Is it the right fit for your context? |
|---|---|---|
| e.g. Layered | UI → Business Logic → Data Access | Was right at start, now teams collide in the layers |
If you can't name the pattern, you're using one accidentally. That's usually how anti-patterns start.
2. Layered architecture — The classic, its strengths, and why teams outgrow it
The simplest way to organise code is by technical concern — put all the user interface code in one place, all the business logic in another, all the data access in a third.
Layered architecture organises components into horizontal layers, each depending only on the layer below.
┌─────────────────────────┐
│ Presentation Layer │ ← User interface, API controllers
├─────────────────────────┤
│ Business Logic Layer │ ← Domain rules, workflows, validation
├─────────────────────────┤
│ Data Access Layer │ ← Database queries, ORM, repositories
├─────────────────────────┤
│ Database │ ← Tables, views, stored procedures
└─────────────────────────┘
Strengths: simple to understand, clear separation of concerns, easy to start with, good testability.
Weaknesses: change ripples through layers — a database change affects data access, business logic, and presentation. Performance overhead — every request passes through every layer. Encourages treating business logic as thin glue between UI and database. Doesn't scale for teams — all teams work in the same layers, creating merge conflicts.
flowchart TD
LAYERED["Layered Architecture"]
LAYERED --> PROS["✅ Simple · Familiar · Easy to start"]
LAYERED --> CONS["❌ Change ripples · Teams collide · CRUD trap"]
CONS --> WHEN["When to move beyond:\nMultiple teams · Complex domain · Different change rates"]
Layered architecture is the right starting point for many systems. But it becomes a constraint when the system grows beyond what a single team can manage.
Layered = simple and familiar — but change ripples through layers and teams collide.
Try it yourself — The layer test
Look at your codebase. Answer these questions:
| Question | Your answer |
|---|---|
| Can you change a business rule without touching the database layer? | |
| Can you change the UI without touching the business logic? | |
| Do multiple teams work in the same layers? | |
| Does a single feature change require touching all layers? |
If the last two are "yes," layered is becoming a constraint. Time to consider a different decomposition.
3. Microservices — When it works, when it doesn't, and what it actually costs
When a system grows large enough that multiple teams are stepping on each other, and different components need to scale, deploy, and change independently — layered architecture breaks down.
Microservices decompose the system into small, independently deployable services, each owning a specific business capability.
flowchart TD
MS["🔬 Microservices"]
MS --> S1["🛒 Order Service\nOwns: orders, checkout\nDeployed independently"]
MS --> S2["💳 Payment Service\nOwns: payments, refunds\nDeployed independently"]
MS --> S3["📦 Inventory Service\nOwns: stock, reservations\nDeployed independently"]
MS --> S4["📧 Notification Service\nOwns: emails, SMS\nDeployed independently"]
S1 <-->|"API / Events"| S2
S1 <-->|"API / Events"| S3
S2 -->|"Events"| S4
Strengths: team autonomy — each team owns, deploys, and scales independently. Technology diversity — each service can use the best tool for its job. Fault isolation — a failure in one service doesn't bring down the whole system. Independent scaling — scale the hot services without scaling everything.
Costs: distributed systems complexity — network failures, latency, eventual consistency. Operational overhead — service discovery, load balancing, monitoring, logging across services. Data management — no shared database, data ownership is hard, distributed transactions are expensive. Testing complexity — integration testing across services is significantly harder.
flowchart LR
PREQ["Prerequisites for Microservices"]
PREQ --> P1["Clear domain boundaries"]
PREQ --> P2["DevOps maturity"]
PREQ --> P3["Team structure\nOne service, one team"]
PREQ --> P4["Data decomposition"]
PREQ --> P5["Operational capability"]
Here's what catches teams out: they skip the stepping stone. The modular monolith comes first — decompose into modules with clear boundaries, enforce those boundaries, then extract to services when a module genuinely needs independent deployment.
Microservices solve team autonomy and independent scaling — but they trade simplicity for distributed systems complexity.
Try it yourself — The microservices readiness test
Rate each statement from 1 to 5:
| Statement | Rating (1-5) |
|---|---|
| We can draw clean boundaries between our business capabilities | |
| We have CI/CD, monitoring, and infrastructure automation | |
| Each service would have a dedicated owning team | |
| We can decompose our data — each service owns its data | |
| We have observability, tracing, and incident management |
If your total is under 15, you're not ready for microservices. Build the modular monolith first.
4. Event-driven architecture — Decoupling through events and messaging
When components need to react to what other components do — but you don't want them coupled through direct calls — events provide the decoupling mechanism.
Components publish events when something significant happens. Other components subscribe to events they care about and react independently.
flowchart LR
PUB["🛒 Order Service\nPublishes: OrderPlaced"] --> BUS["📢 Event Bus\nRoutes events to subscribers"]
BUS --> S1["💳 Payment Service\nSubscribes: OrderPlaced\n→ Process payment"]
BUS --> S2["📦 Inventory Service\nSubscribes: OrderPlaced\n→ Reserve stock"]
BUS --> S3["📧 Notification Service\nSubscribes: OrderPlaced\n→ Send confirmation"]
BUS --> S4["📊 Analytics Service\nSubscribes: OrderPlaced\n→ Update dashboard"]
The publisher doesn't know who subscribes. Subscribers don't know who publishes. This is maximum decoupling.
Strengths: loose coupling — publishers and subscribers are independent. Scalability — add new consumers without changing the publisher. Responsiveness — components react in real-time to changes. Auditability — the event log is a record of everything that happened.
Costs: eventual consistency — there's a delay between event and reaction. Debugging complexity — tracing a request across events is harder than tracing a call chain. Event ordering — ensuring events are processed in the right order is non-trivial. Duplicate processing — consumers must handle duplicate events.
Every event consumer must handle the same event being delivered more than once. Idempotent: "Reserve 5 units" — if already reserved, do nothing. Non-idempotent: "Add 5 to quantity" — duplicate event adds 10 instead of 5.
Event-driven architecture trades consistency for decoupling — the question is whether your business can tolerate eventual consistency.
Try it yourself — The event test
Think of three cross-component workflows in your system. For each one:
| Workflow | Currently | Could events help? |
|---|---|---|
| e.g. Order → Payment → Shipping | Synchronous API calls — if Payment is slow, Order blocks | Yes — Order publishes OrderPlaced, Payment and Shipping react independently |
If your current approach has components blocking on each other, events would decouple them.
5. CQRS and event sourcing — Separating reads from writes
In many systems, reading data and writing data have very different needs. Reads need to be fast and denormalised. Writes need to be consistent and validated. Using one model for both forces compromises.
CQRS separates the write model from the read model. Event sourcing goes further — instead of storing current state, it stores the sequence of events that produced that state.
flowchart TD
CQRS["📖 CQRS + Event Sourcing"]
subgraph WRITE["✍️ Write Side (Command)"]
CMD["Command\nPlaceOrder"] --> VAL["Validate\nBusiness rules"]
VAL --> EVENT["Emit Event\nOrderPlaced"]
EVENT --> STORE["Event Store\nAppend-only log"]
end
subgraph READ["👁️ Read Side (Query)"]
STORE --> PROJ["Projection\nBuild read models"]
PROJ --> READDB["Read DB\nDenormalised, fast"]
QUERY["Query\nGetOrderSummary"] --> READDB
end
CQRS strengths: read and write models can be optimised independently. Read side can be scaled separately. Multiple read models can serve different query needs.
Event sourcing strengths: complete audit trail — every change is recorded. Can rebuild state at any point in time. Natural fit for event-driven architecture.
Costs: complexity — two models to maintain instead of one. Eventual consistency — read model lags behind write model. Event schema evolution — events are immutable, so schema changes are hard.
Don't use these unless you have the problems they solve. Simple CRUD applications don't need CQRS. Small teams can't justify the operational overhead.
CQRS = separate read from write models. Event sourcing = store events, not state. Powerful but complex — only use when you have the problems they solve.
Try it yourself — The CQRS test
Answer these questions about your system:
| Question | Your answer |
|---|---|
| Are your read patterns very different from your write patterns? | |
| Do you need heavy denormalisation for read performance? | |
| Is a full audit trail a regulatory requirement? | |
| Do you need to answer "what happened and when?" | |
| Is your team large enough to handle two models? |
If the first two are "no," you don't need CQRS. If the middle two are "yes" and the last is "yes," consider it.
6. Hexagonal architecture — Ports, adapters, and testability
When your business logic is tangled with database code, API frameworks, and external service calls, it becomes untestable and impossible to change without breaking everything.
Hexagonal architecture puts the domain logic at the centre, surrounded by ports (interfaces) and adapters (implementations).
flowchart TD
HEX["⬡ Hexagonal Architecture"]
subgraph OUTSIDE["🔌 Outside World"]
WEB["Web UI\nREST API"]
CLI["CLI"]
MSG["Message Queue"]
DB["Database"]
EXT["External API"]
end
subgraph ADAPTERS["🔌 Adapters"]
IN1["Inbound Adapter\nREST Controller"]
IN2["Inbound Adapter\nCLI Handler"]
OUT1["Outbound Adapter\nPostgreSQL Repository"]
OUT2["Outbound Adapter\nPayment Gateway Client"]
end
subgraph CORE["💎 Domain Core"]
PORT_IN["Inbound Port\nUse Case Interface"]
DOM["Domain Model\nBusiness rules\nEntities · Value objects"]
PORT_OUT["Outbound Port\nRepository Interface"]
end
WEB --> IN1
CLI --> IN2
IN1 --> PORT_IN
IN2 --> PORT_IN
PORT_IN --> DOM
DOM --> PORT_OUT
PORT_OUT --> OUT1
PORT_OUT --> OUT2
OUT1 --> DB
OUT2 --> EXT
The key insight: the domain core doesn't depend on anything outside. It defines ports (interfaces), and adapters implement those ports. You can swap the database adapter without touching the domain. You can swap the web framework without touching the domain.
Strengths: testability — domain can be tested without databases, APIs, or frameworks. Technology independence — swap any external dependency without changing the core. Clear boundaries — domain logic is isolated from infrastructure concerns. Multiple entry points — the same domain can be accessed via REST, CLI, messaging.
Costs: indirection — more interfaces and abstractions to maintain. Learning curve — the pattern is less intuitive than layered for junior developers. Over-engineering risk — for simple CRUD, hexagonal adds complexity without benefit.
Hexagonal architecture protects the domain from everything else — the domain is the asset, everything else is replaceable.
Try it yourself — The domain test
Look at your business logic. Can you answer these questions?
| Question | Your answer |
|---|---|
| Can you test your domain rules without a database? | |
| Can you swap your web framework without changing your domain? | |
| Can you swap your database without changing your domain? | |
| Is your domain logic isolated from infrastructure code? |
If any answer is "no," your domain is tangled with infrastructure. Hexagonal architecture untangles it.
7. Serverless — Where control is traded for speed
Managing servers, scaling, patching, and capacity planning is undifferentiated heavy lifting — it doesn't create business value but consumes engineering time.
Serverless and platform-based approaches trade control for convenience. The provider manages the infrastructure; you focus on the code.
flowchart TD
SV["⚡ Serverless & Platform"]
SV --> FAAS["☁️ FaaS\nFunction as a Service\nAWS Lambda · Azure Functions\nNo servers · Pay per execution"]
SV --> PAAS["⚙️ PaaS\nPlatform as a Service\nHeroku · App Engine · Railway\nFocus on code · Platform manages rest"]
SV --> BaaS["📦 BaaS\nBackend as a Service\nSupabase · Firebase · Auth0\nPre-built backend services"]
FAAS --> TRADE1["Trade: Control → Convenience\nCold starts · Vendor lock-in\nExecution time limits"]
PAAS --> TRADE2["Trade: Flexibility → Simplicity\nLess customisation\nScaling is provider-managed"]
BaaS --> TRADE3["Trade: Ownership → Speed\nVendor-dependent\nLimited customisation"]
When serverless works well: event-driven workloads — process an upload, respond to a webhook, handle a queue message. Bursty traffic — unpredictable load that would be expensive to provision for. Small, stateless functions — one function, one job.
When serverless doesn't work: long-running processes — most platforms limit execution time. High-throughput, consistent load — becomes more expensive than dedicated compute. Stateful workloads — no persistent connections or local state. Latency-sensitive — cold starts add unpredictable latency.
Serverless isn't "no servers" — it's "someone else's servers, someone else's scaling, someone else's patching."
Try it yourself — The serverless fit test
Think of three workloads in your system. For each one:
| Workload | Event-driven? | Bursty? | Stateless? | Serverless fit? |
|---|---|---|---|---|
| e.g. Image resize on upload | Yes | Yes | Yes | Perfect fit |
If all three columns are "yes," serverless is worth considering. If any is "no," think carefully.
8. Choosing a pattern — Matching context to structure, not hype to architecture
There is no universally best pattern. The right pattern depends on your context — your team, your domain, your constraints, your scale.
Pattern selection is a decision under constraints. Use this framework:
flowchart TD
CTX["📋 Your Context"]
CTX --> Q1{"How many teams?"}
Q1 -->|"1-2"| MONO["Monolith or modular monolith"]
Q1 -->|"3-8"| MOD["Modular monolith or selective services"]
Q1 -->|"8+"| MICRO["Microservices or domain services"]
CTX --> Q2{"How complex is the domain?"}
Q2 -->|"Simple CRUD"| LAY["Layered is fine"]
Q2 -->|"Rich domain"| HEX["Hexagonal or onion"]
CTX --> Q3{"Do components need to react?"}
Q3 -->|"Yes"| EDA["Event-driven"]
Q3 -->|"No"| SYN["Synchronous integration"]
CTX --> Q4{"Read/write patterns different?"}
Q4 -->|"Yes"| CQRS_PAT["CQRS"]
Q4 -->|"No"| UNIFIED["Unified model"]
The decision matrix:
| Context factor | Favours | Because |
|---|---|---|
| Small team, simple domain | Layered / monolith | Low overhead, fast to start |
| Multiple teams, complex domain | Microservices / hexagonal | Team autonomy, domain protection |
| Event-driven business processes | Event-driven architecture | Natural fit for reactive workflows |
| Different read/write needs | CQRS | Optimise each side independently |
| Need audit trail | Event sourcing | Events are the audit log |
| Bursty, event-triggered workload | Serverless | Pay per execution, no idle cost |
| Need testability and technology independence | Hexagonal | Domain isolated from infrastructure |
Here's what real systems look like. Hexagonal structure per service — domain protection. Microservices deployment — team autonomy. Event-driven integration — loose coupling. CQRS for read-heavy components — performance. Serverless for event processing — cost efficiency.
The best pattern is the one whose trade-offs match your constraints — not the one trending on social media.
Try it yourself — The pattern match
Take your system's context. Run it through the matrix:
| Context factor | Your situation | Pattern it favours |
|---|---|---|
| Team size | ||
| Domain complexity | ||
| Scaling needs | ||
| Consistency requirements | ||
| Event-driven workflows? |
Does your current architecture match the pattern your context favours? If not, that's your migration path.
Putting it all together
Here's the complete picture in one diagram. This is the mental model worth internalising.
flowchart TD
CTX["📋 Your Context\nTeam size · Domain complexity\nScaling needs · Consistency requirements"]
CTX --> EVAL["⚖️ Evaluate Trade-offs\nWhat do you gain?\nWhat do you sacrifice?"]
EVAL --> CHOOSE["🎯 Choose Pattern(s)\nLayered · Microservices\nEvent-driven · CQRS\nHexagonal · Serverless"]
CHOOSE --> COMBINE["🔗 Combine\nReal systems use multiple\npatterns at different layers"]
COMBINE --> ADAPT["🔄 Adapt\nPatterns evolve as the\nsystem and context change"]
The foundation:
Every pattern solves something and sacrifices something else. There is no best pattern — only trade-offs that match your context. Real systems combine patterns — hexagonal for domain logic, event-driven for integration, CQRS for read-heavy components, serverless for event processing. Start simple, evolve deliberately. Begin with a modular monolith. Extract services when the need is real, not aspirational.
Cheat Sheet — All the key terms
| Pattern | One-Line Memory | Best For |
|---|---|---|
| Layered | Simple and familiar | Small teams, CRUD apps, starting point |
| Modular monolith | Structured, single deployment | Growing teams not yet ready for microservices |
| Microservices | Independent teams and deployment | Large teams, diverse scaling needs |
| Event-driven | Maximum decoupling | Reactive business processes, fan-out |
| CQRS | Read/write separated | Different read/write patterns, performance |
| Event sourcing | Store events, not state | Audit trail, replay, temporal queries |
| Hexagonal | Domain at the centre | Complex domains, testability, technology independence |
| Serverless | No servers, pay per use | Event-driven, bursty, stateless |
How to know if this landed
You'll know this has landed when someone stops asking "should we do microservices?" and starts asking "what problem are we trying to solve, and which pattern's trade-offs match our context?" They can name the strengths and costs of each pattern. They can explain why their system uses the patterns it does — or why it should change. They combine patterns deliberately at different layers instead of forcing one pattern everywhere. And they start simple, evolving deliberately as the system grows.
What changes when the mental model clicks
I've run this session with teams who chose microservices because it was trending and spent eighteen months fighting distributed systems complexity. The gap at the start is usually not about understanding the patterns — it's about matching pattern trade-offs to context.
What changes after this session:
Teams stop asking "which pattern is best?" and start asking "which pattern's trade-offs match our context?" The pattern match exercise — running their situation through the decision matrix — is always the moment things click. People stop treating patterns as prescriptions and start treating them as starting points. Their results get better. They stop blaming the pattern when the real problem was that they applied it in the wrong context.
The combination exercise tends to immediately change how teams think about their architecture. They start seeing that hexagonal per service, event-driven between services, CQRS for read-heavy components isn't contradictory — it's using each pattern where it's strongest.
Book a Workshop
Ready to choose the right patterns for your context?
or
1.5-day workshop includes pattern assessment for your current architecture, context analysis — what patterns fit your team, domain, and constraints, trade-off evaluation — what does each pattern cost you, anti-pattern identification — find the structural mistakes in your system, pattern combination exercise — design a real architecture using multiple patterns, and evolution planning — how to move from your current pattern to your target pattern.