System Design Patterns You Should Know

System Design Patterns You Should Know

Share
System DesignPatterns
4 min read

1. Monolith vs Microservices

Pattern 1 — Monolith vs Microservices. Two architectures side by side: a single server containing everything on the left, versus three isolated services communicating over the network on the right.

Pattern 1 — Monolith vs Microservices. Two architectures side by side: a single server containing everything on the left, versus three isolated services communicating over the network on the right.

The first decision in any system design: how do you structure your backend?

Monolith

  • All features (auth, posts, notifications, etc.) live in a single codebase on one server.
  • You can still run multiple instances of it for scale — but you scale the whole thing or nothing.
  • Easy to manage, but a crash in one feature (e.g. notifications) can take down the entire server.

Microservices

  • Each feature is its own independent service, potentially written in a different language and deployed on different infrastructure.
  • Services can be scaled, deployed, and updated independently.
  • If the notification service crashes, auth and posts keep running.
  • Trade-off: managing multiple services is more complex, and you now need to handle inter-service communication.

Rule of thumb: start with a monolith, migrate to microservices as scale demands.


2. Database per Service

Once you go microservices, the next question is: do they share a database?

Pattern 2 — Database per Service. Each microservice owns a dedicated database, chosen to fit its workload. No shared tables, no cross-service writes.

Pattern 2 — Database per Service. Each microservice owns a dedicated database, chosen to fit its workload. No shared tables, no cross-service writes.

Shared database (anti-pattern at scale)

  • All services read from and write to the same DB.
  • Looks convenient — but any service can accidentally read or delete another service's data. There's no true isolation.

Database per service

  • Each microservice gets its own database, chosen to fit its needs:
  • True isolation: services can't touch each other's data directly.
  • Trade-off: no database-level JOINs across services. If the post service needs user details, it must call the auth service via an API and join the data in application code (application-level joins).

3. Circuit Breaker Pattern

The problem: cascading failures

When services depend on each other, one failing service can trigger a chain reaction — taking down every service that depends on it.

The solution: a circuit breaker proxy

You put a circuit breaker between services. It has three states:

StateMeaning
ClosedNormal operation. Requests flow through.
OpenDownstream service is failing. Requests are blocked immediately (no wasted retries). Returns a fallback response.
Half-openAfter a timeout, the breaker lets a test request through. If it succeeds, the circuit closes again.

This prevents retry storms from piling onto an already struggling service, and allows that service time to recover.

Real-world implementations: Netflix Hystrix, Resilience4j, AWS App Mesh.

Pattern 3 — Circuit Breaker. The breaker sits as a proxy between two services and moves through three states — Closed (healthy), Open (blocking), and Half-open (probing) — to prevent a failing downstream service from cascading failures upstream.

Pattern 3 — Circuit Breaker. The breaker sits as a proxy between two services and moves through three states — Closed (healthy), Open (blocking), and Half-open (probing) — to prevent a failing downstream service from cascading failures upstream.


4. Event Sourcing

The problem with mutable state

A traditional order table stores the current state:

order_id | status
1        | "shipped"

You keep overwriting it. At scale (think Amazon), acquiring locks on this row for every update becomes a bottleneck.

Event sourcing: append-only log

Instead of storing the current state, you store an immutable log of every event that happened:

12:00 — order_placed
12:10 — ready_to_ship
12:30 — shipped
12:45 — delivered
  • Nothing is ever updated or deleted — only appended.
  • To find current state, replay the log and read the latest entry.
  • Banking works this way: your balance isn't stored as a single number. It's computed from a ledger of every debit and credit transaction.

Benefits

  • No locks needed → scales much better
  • Full audit trail by default
  • Easy to rebuild state at any point in time
Pattern 4 — Event Sourcing. Instead of a mutable state column, every change is appended as an immutable event to a log. Current state is always derived by replaying that log.

Pattern 4 — Event Sourcing. Instead of a mutable state column, every change is appended as an immutable event to a log. Current state is always derived by replaying that log.


5. CQRS (Command Query Responsibility Segregation)

Pairs naturally with Event Sourcing.

At Amazon-scale, even a single database becomes a bottleneck when it handles both reads and writes simultaneously.

The idea: split read and write paths

  • Command side (writes): handles create, update, delete operations. Each command creates an event in the event log.
  • Query side (reads): a separate, denormalized database optimized purely for fast reads. It gets updated by consuming events from the command side.
User action → Command → Event log → [async sync] → Read DB → User query

Trade-off: the two sides are not always in sync immediately — this is eventual consistency. If your system can tolerate a small lag between writing and reading, CQRS is a strong pattern.

Pattern 5 — CQRS. Writes (commands) and reads (queries) flow through entirely separate paths. Commands produce events; those events asynchronously update a dedicated read-optimized store

Pattern 5 — CQRS. Writes (commands) and reads (queries) flow through entirely separate paths. Commands produce events; those events asynchronously update a dedicated read-optimized store

Quick Reference

PatternProblem it solvesKey trade-off
MicroservicesScaling and isolationOperational complexity
Database per serviceTrue service isolationApplication-level joins
Circuit breakerCascading failuresSlight added latency overhead
Event sourcingMutable state bottlenecks, audit trailsComplexity in querying current state
CQRSRead/write bottleneck at scaleEventual consistency

Share this article