Skip to main content

Command Palette

Search for a command to run...

Architecture Patterns: Wrap-Up

Updated
12 min read
Architecture Patterns: Wrap-Up

Series: System Design · Architecture Patterns — Pillar 7 of 8

Systems Design

# Post What it covers
00 Architecture Patterns: How Systems Are Structured Twenty patterns covering monoliths, microservices, events, resilience, deployment, and data processing. How to structure systems that survive growth.
01 Monolithic Architecture: The Default That Gets Abandoned Too Early Monoliths are fast to build and easy to operate. Learn when they're the right choice, when they break down, and how to know the difference.
02 Microservices: The Architecture You Earn, Not Choose Microservices enable independent scaling and team autonomy — but at significant cost. Learn what you actually get, what you pay, and when it's worth it.
03 Serverless: Pay for What You Use, Not What You Provision Serverless scales to zero and charges per invocation. Learn where it shines, where it fails, and how to design around cold starts and vendor lock-in.
04 Event-Driven Architecture: Decoupling Through Events Event-driven systems communicate via events rather than direct calls. Learn how producers, consumers, and event brokers work — and the consistency tradeoffs involved.
05 Message Queues: Decoupling Produce from Consume Message queues decouple producers and consumers, enable load levelling, and provide durability. Learn how they work and when to use Kafka vs SQS vs RabbitMQ.
06 Pub/Sub: Broadcasting Events to Multiple Consumers Pub/sub decouples publishers from subscribers through topics. Learn how it differs from message queues and when to use Kafka, SNS, or Google Pub/Sub.
07 CQRS: When Reads and Writes Need Different Models CQRS separates writes from reads so each can be optimised independently. Learn how it works, when it's worth the complexity, and when it isn't.
08 Event Sourcing: The Ledger, Not the Balance Event sourcing stores state as a sequence of events. Learn how it works, what you get (audit log, time travel), and what it costs (complexity, schema evolution).
09 The Saga Pattern: Distributed Transactions Without Locks The Saga pattern manages distributed transactions across services using compensating transactions. Learn choreography vs orchestration and when to use each.
10 The Outbox Pattern: Atomic Writes and Event Publishing The Outbox pattern solves the dual-write problem — publishing an event and writing to a database atomically. Learn how it works using CDC or polling.
11 The Circuit Breaker: Stopping Cascading Failures Circuit breakers prevent cascading failures by fast-failing calls to unhealthy dependencies. Learn the three states, how to configure them, and where to apply them.
12 The Bulkhead Pattern: Containing Failures Through Resource Isolation Bulkheads isolate thread pools and connections per dependency so one failure can't exhaust resources needed by others. Learn how to apply them in practice.
13 The Sidecar Pattern: Cross-Cutting Concerns Without Code Changes The sidecar pattern deploys a helper process alongside each service for logging, metrics, TLS, and service discovery — without modifying the service itself.
14 Service Mesh: A Programmable Network for Microservices A service mesh handles service-to-service traffic, mTLS, circuit breaking, and observability via a fleet of sidecar proxies. Learn how it works and when to use it.
15 Service Discovery: Finding Services in a Dynamic Environment Service discovery lets services find each other in dynamic environments. Learn client-side vs server-side discovery, health checks, and DNS vs registry approaches.
16 The Strangler Fig: Replacing a Legacy System Without Burning It Down The Strangler Fig replaces a legacy system incrementally by routing specific functionality to new implementations while the old system keeps running.
17 Backend for Frontend: One API Per Client Type BFF creates dedicated API backends per client type. Learn why one general API struggles to serve mobile and web well, and how BFF solves it.
18 ETL Pipelines: Moving Data from Operations to Analytics ETL moves data from operational systems into analytical stores. Learn how pipelines work, what ELT is, and how to design reliable data movement at scale.
19 Batch vs Stream Processing: How Fresh Do Your Answers Need to Be? Batch processes accumulate data then processes in bulk; streaming processes each event as it arrives. Learn the tradeoffs and when each is right.
20 MapReduce: Processing Petabytes in Parallel MapReduce processes massive datasets in parallel by splitting work into map and reduce phases. Learn how it works and why Spark has largely replaced it.
21 Architecture Patterns: Wrap-Up ← you are here A recap of all 20 architecture patterns across decomposition, async communication, data patterns, resilience, and data processing. How they connect.

Architecture Patterns: Wrap-Up

Twenty patterns across five themes. This post ties them together — not as a glossary, but as a connected set of design decisions that compound across a real system.


The one thing to remember from each post

Monolithic — the right default for most systems. Start here. Move away only when coordination tax between teams or component-level scaling requirements become the actual constraint.

Microservices — independent deployability and scaling at the cost of distributed systems complexity. Every service call can fail; every cross-service operation needs retry, circuit breaking, and distributed tracing. Only worth it at a specific scale and team size.

Serverless — pay per invocation, scale to zero, no server management. Correct for infrequent, bursty, or event-triggered workloads. Wrong for sustained high-throughput endpoints or anything with cold start latency requirements.

Event-Driven Architecture — services communicate through events rather than direct calls. Decouples producers from consumers, enables fan-out, and improves resilience. The cost is eventual consistency and harder debuggability.

Message Queues — durable, ordered buffers that decouple produce rate from consume rate. One consumer per message. At-least-once delivery means consumers must be idempotent. Dead letter queues catch messages that fail repeatedly.

Pub/Sub — topics receive messages from publishers and fan out to all subscribers independently. Each subscriber is a different consumer with its own position in the stream. The broadcast model for event-driven architectures.

CQRS — separate write model (commands, normalised, transactional) from read model (queries, denormalised, pre-computed). Each is optimised independently. The read model is eventually consistent with the write model.

Event Sourcing — store the sequence of events rather than current state. Current state is derived by replay. Complete audit trail and temporal queries come free. Schema evolution and query complexity are the price.

Saga — distributed transactions across services using local transactions and compensating actions. No distributed lock. Choreography (event-driven) or orchestration (central coordinator). Every forward step needs an explicit compensating action.

Outbox Pattern — write a message to a database "outbox" table in the same transaction as the entity. A relay process publishes from the outbox to the broker. Solves the dual-write problem atomically without a distributed transaction.

Circuit Breaker — monitors calls to a dependency; opens when failure rate exceeds threshold; fast-fails subsequent calls; tests recovery in half-open state. Prevents cascade failures. Not optional in microservices.

Bulkhead — dedicated resource pools (threads, connections) per dependency. Slow or failing dependencies can't exhaust resources needed by others. Complementary to circuit breakers.

Sidecar — co-deployed helper process handling cross-cutting concerns (logging, metrics, mTLS, service discovery) so the main service focuses on business logic. The foundation of service meshes.

Service Mesh — data plane of sidecar proxies managed by a control plane. Provides mTLS everywhere, circuit breaking, traffic routing, and distributed tracing without application code. High operational complexity; justified at scale.

Service Discovery — dynamic lookup of service addresses in environments where instance IPs change constantly. Kubernetes Services + CoreDNS provide this automatically. Consul for non-Kubernetes environments.

Strangler Fig — incrementally replace a legacy system by routing modules to new implementations via a facade, while the legacy system continues running. Each step is small and reversible. Requires discipline to complete.

Backend for Frontend — dedicated API backend per client type. Each BFF aggregates from downstream services and shapes the response for its client's exact needs. Owned by the client team.

ETL Pipelines — extract from operational sources, transform to analytical schema, load to a data warehouse. Enables analytical queries without affecting operational performance. ELT (load then transform) is the modern pattern with capable data warehouses.

Batch vs Stream Processing — batch accumulates then processes in bulk (latency = batch interval). Stream processes each event immediately (latency = seconds). Choose based on how fresh the results must be; don't over-engineer freshness that isn't needed.

MapReduce — split a massive dataset across machines (map phase: process each partition independently), shuffle results by key, aggregate (reduce phase). Hadoop MapReduce wrote to disk between phases; Spark keeps data in memory and is 10–100x faster for multi-stage jobs.


How they connect

Decomposition patterns set the context for everything else. The monolith is a single unit — patterns like service discovery, circuit breakers, and service mesh don't apply. Microservices require all of them. Serverless is orthogonal to both, applicable to specific workloads regardless of overall architecture.

Async communication patterns (event-driven, message queues, pub/sub) work together: pub/sub provides the fan-out layer; message queues provide the per-consumer buffer; event-driven architecture is the architectural style that combines them. CQRS and Event Sourcing depend on these: events flow from the write model to update read projections.

Saga + Outbox are inseparable in practice. A saga's events and commands must be reliably delivered — the Outbox pattern provides that guarantee. Without the Outbox, saga steps can be lost in the dual-write window, leaving the saga in an inconsistent intermediate state permanently.

Circuit breaker + bulkhead + sidecar = the resilience stack. In a microservices system, these three are the minimum viable resilience pattern. Circuit breakers prevent cascade failures; bulkheads contain resource exhaustion; sidecars (or their service mesh equivalent) implement both consistently across all services without application code.

Service mesh + service discovery are operational concerns. They're infrastructure for microservices, not business logic. They become necessary at a certain scale of service count and team size; before that, a load balancer and hardcoded service URLs work fine.

Strangler Fig + BFF often appear together. Migrating a legacy monolith with the Strangler Fig pattern often requires creating BFFs — the new implementations can have client-specific APIs that weren't possible in the monolith.

ETL + batch vs stream + MapReduce form the data processing stack. ETL defines the movement; batch vs stream defines the freshness model; MapReduce (Spark) is the compute framework for large-scale batch transformations.


End-to-end: the URL shortener's full architecture

Clients (Mobile, Web, API partners)
  ↓
BFF Layer:
  Mobile BFF → optimised mobile responses
  Web BFF → rich web dashboard responses
  Public API → stable partner API
  ↓ (all three call downstream services)

Service Layer (microservices, extracted from monolith via Strangler Fig):
  Redirect Service (Go) — latency-critical, stateless, 1M req/s
  Link Service (Rails) — CRUD, saga-based creation workflow
  Analytics Service (Python) — stream + batch, CQRS read models
  User Service (Rails) — accounts, auth
  Billing Service (Rails) — subscription, invoicing
  Notification Service (Node) — email, Slack, webhooks

Cross-service:
  All services: circuit breakers + bulkheads on outbound calls
  All services: Envoy sidecar (distributed tracing, mTLS, metrics)
  Service mesh: Istio control plane
  Service discovery: Kubernetes Services + CoreDNS

Async communication:
  Kafka topics: link.created, link.clicked, user.upgraded, billing.invoiced
  Outbox pattern: every Kafka publish backed by PostgreSQL outbox
  Pub/Sub fan-out: link.created → analytics, QR, billing, webhooks
  Saga: UpgradeSubscription saga with Billing → User → Link → Notification

Data processing:
  Stream: Kafka + Flink for real-time fraud detection and live counters
  Batch: nightly Spark job for full analytics aggregation
  ETL: Debezium CDC from PostgreSQL → Snowflake data warehouse
  MapReduce (Spark): monthly billing computation, historical replays

Legacy:
  Original Rails monolith still handles ~30% of functionality (pending migration)
  Strangler Fig facade routes new paths to new services

The decision tree

How many engineers on the project?
  < 20 → Monolith (strong default)
  > 20, genuinely blocked on coordination → Microservices selectively

Is this function infrequent, bursty, or event-triggered?
  Yes → Consider Serverless (Lambda, Cloud Functions)

Do multiple services need to react to the same event?
  Yes → Pub/Sub (Kafka, SNS) + per-consumer queues (SQS)
  No, one consumer → Message Queue (SQS, RabbitMQ)

Do reads and writes need different data shapes or scaling?
  Yes → CQRS; if full audit trail required → Event Sourcing

Does a business operation touch multiple services' databases?
  Yes → Saga pattern for orchestration
  Must publish events atomically with DB writes → Outbox Pattern

Does your service call external dependencies?
  Yes → Circuit Breaker + Bulkhead (mandatory in microservices)

Do you have 5+ services with cross-cutting concerns?
  Yes → Sidecar pattern; 10+ services → Service Mesh

Do you have a legacy system to replace?
  → Strangler Fig (incremental migration, never big-bang rewrite)

Do different client types need different API shapes?
  → Backend for Frontend (one BFF per client type)

Do you need data in an analytical store?
  → ETL Pipeline (CDC for near-real-time, batch for daily)
  How fresh? < 1 min → Stream (Flink); > 1 hour → Batch (Spark)

Up next: Pillar 8 — Distributed Systems

Architecture patterns define how services are structured. Distributed systems covers what happens inside and between those services when things go wrong at the network and consensus level — the problems that emerge when you have multiple machines that must agree on something.

Twenty concepts: Network Partitions, Split-Brain, Heartbeats, Leader Election, Consensus Algorithms, Quorum, Paxos, Raft, Gossip Protocol, Logical Clocks, Lamport Timestamps, Vector Clocks, Distributed Transactions, Two-Phase Commit, Three-Phase Commit, Delivery Semantics, Change Data Capture, Erasure Coding, Merkle Tree, and Observability/Tracing/Metrics.

**← Previous: MapReduce — the programming model behind batch processing at scale; how massive datasets are processed in parallel across hundreds of machines.*


*← Previous: MapReduce — the programming model behind batch processing at scale; how massive datasets are processed in parallel across hundreds of machines.*This is the end of the Architecture Patterns pillar. Continue to Pillar 8 — Distributed Systems

Systems Design

Part 1 of 50

Understanding these system design concepts is essential for architects, developers, and engineers to create scalable, reliable, and maintainable software systems that meet the needs of businesses.