DEV Community

Cover image for Keeping Data Correct Across Microservices (Explained with Swiggy)
Vignesh Athiappan
Vignesh Athiappan

Posted on

Keeping Data Correct Across Microservices (Explained with Swiggy)

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
Enter fullscreen mode Exit fullscreen mode
✅ 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode
✅ 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
Enter fullscreen mode Exit fullscreen mode
✅ 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
Enter fullscreen mode Exit fullscreen mode

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)