Here's the catch nobody warns you about: the moment you split an app into microservices, each one owns its own database — and suddenly there's no single source of truth. An order lives in one DB, its payment in another, its delivery in a third. So how do you keep everything correct? These 5 patterns are the answer.
The 5 patterns
| # | Pattern | One-line |
|---|---|---|
| 1 | Database-per-service | Each service owns its data, period |
| 2 | Saga | Manage a transaction that spans many services |
| 3 | CQRS | Separate the write model from the read model |
| 4 | Event Sourcing | Store changes, not current state |
| 5 | Outbox | Reliably publish events without losing them |
1. Database-per-service
Each microservice owns its own database. No other service touches it directly.
[Order svc] → Order DB ❌ Payment svc CANNOT read Order DB directly
[Payment svc] → Payment DB it must ASK the Order service
| ✅ Good | ❌ Bad |
|---|---|
| Services are truly independent | No easy JOIN across services |
| Change one DB freely | Data is spread out, harder to keep consistent |
| Fault isolation | You need the patterns below to fix consistency |
Swiggy: Order, Payment, and Delivery each own their DB. This is why the next four patterns exist — it creates the consistency problem they solve.
⚠️ The golden rule: never let one service reach into another's database. Always go through its API or events.
2. Saga — the big one
A transaction that spans multiple services, done as a chain of steps with undo actions.
Normally a database transaction is all-or-nothing. But across 4 services with 4 databases, you can't wrap one giant transaction around everything. So a Saga does it as steps — and if one fails, it compensates (undoes) the earlier ones.
Place order:
✅ Order created
✅ Payment charged
❌ No delivery partner available!
→ COMPENSATE: refund payment → cancel order
Two flavors:
| Choreography | Orchestration | |
|---|---|---|
| How | Each service reacts to events, no boss | One coordinator directs each step |
| Analogy | Dancers who know their cues | A conductor leading an orchestra |
| Good for | Simple flows | Complex flows |
| Downside | Hard to trace | The coordinator is a dependency |
| ✅ Good | ❌ Bad |
|---|---|
| Consistency without one giant lock | Complex to design |
| Each step can retry | You must write "undo" logic for everything |
Swiggy: the classic "order stuck → auto-refund" flow. Payment succeeded but no rider? The Saga triggers the refund (a compensating transaction).
Azure: Durable Functions (orchestration) or Service Bus (choreography).
3. CQRS — Command Query Responsibility Segregation
Split your data model in two: one for writing, one for reading.
The name sounds fancy; the idea is simple. Writes and reads have different needs, so stop forcing them through one model.
WRITE side (Command): "Place order" → normalized, safe, transactional
READ side (Query): "Show my orders" → denormalized, fast, pre-shaped
The two sides are kept in sync (often via events).
| ✅ Good | ❌ Bad |
|---|---|
| Reads are blazing fast (pre-built views) | Two models to maintain |
| Scale reads and writes separately | Eventual consistency between them |
| Each side optimized for its job | Overkill for simple apps |
Swiggy: writing an order needs strict validation. Reading "your past orders" needs to be instant. CQRS lets the read side be a fast, pre-shaped view — no heavy JOINs at read time.
⚠️ Don't reach for CQRS unless read and write needs really diverge. It adds real complexity.
4. Event Sourcing
Instead of storing the current state, store every change as an event. Rebuild state by replaying them.
❌ Normal: balance = ₹500 (you only see the final number)
✅ Event Sourced: +₹1000 deposited
-₹300 order
-₹200 order
→ replay = ₹500 (and you have the FULL history)
| ✅ Good | ❌ Bad |
|---|---|
| Complete audit trail (every change kept) | Complex to build |
| Can rebuild state anytime | Replaying can get slow (needs snapshots) |
| "Time travel" — see state at any past point | Steep learning curve |
Swiggy: an order's life — placed → confirmed → cooking → picked up → delivered. Store each as an event and you get the entire timeline for free (great for support and disputes), not just "delivered."
Pairs with CQRS: the events are the write side; you build fast read views from them.
5. Outbox Pattern
Guarantees you never lose an event when you save data and publish a message.
Here's the hidden bug it fixes: you save an order to the DB, then publish "order placed" to the queue — but the app crashes between those two steps. Order saved, event lost, payment never happens. 💥
❌ Risky: save order → 💥 crash 💥 → publish event (never runs)
✅ Outbox: save order + event in ONE DB transaction
→ a separate process reads the "outbox" table → publishes reliably
| ✅ Good | ❌ Bad |
|---|---|
| Never lose an event | Extra table + a publisher process |
| Data + event saved atomically | Slight delay before publishing |
Swiggy: "order placed" MUST reach payment. The outbox writes the order and the event together in one transaction, so a crash can't drop it.
How they fit together
These aren't 5 random tools. They're layers solving one chain of problems created by splitting the database:
Database-per-service → creates the consistency problem
↓
Saga → keeps a multi-service transaction consistent
Outbox → makes sure the events driving it never get lost
CQRS → fast reads across all that split data
Event Sourcing → full history + feeds the CQRS read side
The whole thing in one line
Splitting the database is what makes microservices powerful and what makes data hard. Saga keeps multi-service actions consistent, Outbox makes their events reliable, CQRS makes reads fast, and Event Sourcing keeps the full history. Reach for each only when the problem it solves is actually yours.
Top comments (0)