The Distributed Transaction Problem
One Order, Many Databases
In the ShopEase monolith, placing an order was one @Transactional method: insert the order, reduce stock, record the payment. If anything failed, the database rolled everything back.
After the split, each service owns its own database (database per service). One business action now touches several databases that no single transaction can cover:
placeOrder()
|
+--> order-service --> order_db (INSERT orders)
+--> inventory-service --> inventory_db (UPDATE stock)
+--> payment-service --> payment_db (INSERT payments) + Razorpay API
@Transactional only works inside ONE database connection.
If payment fails AFTER stock was reduced, who puts the stock back?Why Not Two-Phase Commit (2PC)?
Two-phase commit (XA transactions) asks every database to "prepare", then tells all of them to "commit". It gives strong consistency, but it is a poor fit for microservices:
- Blocking: rows stay locked while the coordinator waits for the slowest participant.
- Availability: if the coordinator or one service is down, nobody can commit.
- Limited support: Kafka, MongoDB, most cloud databases and every external API (Razorpay, Shiprocket) cannot join an XA transaction.
- Coupling: all services must be up at the same time for every order.
Microservices accept eventual consistency instead: each service commits its own local transaction, and the system reaches a consistent state a little later, using a Saga.
What is a Saga?
A Saga is a sequence of local transactions. Each step commits in its own service and triggers the next step. If a step fails, the saga runs compensating transactions that undo the earlier steps in reverse order.
| Step (local transaction) | Service | Compensation (undo) |
|---|---|---|
| Create order with status PENDING | order-service | Mark order CANCELLED |
| Reserve 2 units of stock | inventory-service | Release the 2 units |
| Charge the customer | payment-service | Refund the payment |
| Create shipment | shipping-service | (last step: nothing after it can fail) |
Note: A compensation is a new business action, not a database rollback. A refund is a real transaction the customer can see in their bank statement. That is why each step must be designed with its undo in mind.
| Approach | Consistency | Coordination | Used for |
|---|---|---|---|
| Local @Transactional | Strong (ACID) | One database | Work inside one service |
| 2PC / XA | Strong | Blocking coordinator | Rare; legacy systems with XA databases |
| Saga | Eventual | Events or an orchestrator | Business flows across services |
Exercise
Exercise: A movie-ticket booking (BookMyShow style) has three steps: hold seats (seat-service), charge the card (payment-service), send the ticket by SMS (notification-service). Write the compensation for each step and say which step should come last.
Show answer
Step 1 hold seats -> compensation: release the seats
Step 2 charge the card -> compensation: refund the amount
Step 3 send ticket SMS -> no compensation needed
The SMS goes last: it cannot be "un-sent", and nothing after it can fail.
Put steps that cannot be undone (pivot / non-compensatable steps) as late as possible.