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.
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.
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:
| State | Meaning |
|---|---|
| Closed | Normal operation. Requests flow through. |
| Open | Downstream service is failing. Requests are blocked immediately (no wasted retries). Returns a fallback response. |
| Half-open | After 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.
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.
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
Quick Reference
| Pattern | Problem it solves | Key trade-off |
|---|---|---|
| Microservices | Scaling and isolation | Operational complexity |
| Database per service | True service isolation | Application-level joins |
| Circuit breaker | Cascading failures | Slight added latency overhead |
| Event sourcing | Mutable state bottlenecks, audit trails | Complexity in querying current state |
| CQRS | Read/write bottleneck at scale | Eventual consistency |
Share this article