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)ServiceCompensation (undo)
Create order with status PENDINGorder-serviceMark order CANCELLED
Reserve 2 units of stockinventory-serviceRelease the 2 units
Charge the customerpayment-serviceRefund the payment
Create shipmentshipping-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.

ApproachConsistencyCoordinationUsed for
Local @TransactionalStrong (ACID)One databaseWork inside one service
2PC / XAStrongBlocking coordinatorRare; legacy systems with XA databases
SagaEventualEvents or an orchestratorBusiness 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.