Skip to content
ArchitectureSolution DesignPatternsMicroservicesEvent Driven

Common Architecture Patterns

Level:Intermediate (SA, EA)
Duration:1.5-day workshop
Deliverable:Pattern selection guide + pattern comparison matrix + anti-pattern catalogue

Quick Navigation


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?

→ Book a Training Session

or

→ Contact me directly

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.

Related Trainings

Next Step

Run this with your team

Every programme is adapted to your context before delivery — your systems, your constraints, your decisions. A short call is enough to work out the right shape and scope.