Why Local Transactions Fail
Every chapter until now assumed one database. This chapter is the problem statement for the rest of Part 7 — the moment "just wrap it in @Transactional" stops being an answer.
◆ Story
An e-commerce checkout now spans four independent microservices, each with its own database: Order Service (creates the order), Payment Service (charges the card), Inventory Service (reserves stock), and Notification Service (sends a confirmation). Placing an order needs all four to succeed together, or for the ones that already succeeded to be undone if a later one fails — the exact same all-or-nothing requirement from Chapter 01's bank story, except now spread across four separate databases that have never heard of each other.
◆ Under the hood — the actual technical reason
Chapter 10 established that @Transactional is backed by a single PlatformTransactionManager, ultimately controlling a single database connection's BEGIN / COMMIT. A single JDBC connection is fundamentally scoped to one database. There is no way to open one connection that spans Order Service's Postgres database, Payment Service's separate Postgres database, and Inventory Service's separate MySQL database, and issue one COMMIT that atomically applies to all three — the transaction primitive itself, at the database protocol level, doesn't support that. This isn't a Spring limitation or something a bigger annotation could fix; it's a fundamental property of how database transactions work.
@Transactional // this ONLY controls Order Service's own database connection public void placeOrder(OrderRequest request) { orderRepository.save(toOrder(request)); // Order Service's DB — covered by @Transactional paymentClient.charge(request.paymentDetails()); // a REST/gRPC call to a DIFFERENT service's DB — // completely outside this transaction's control inventoryClient.reserveStock(request.items()); // also outside — a network call, not a local operation // if inventoryClient.reserveStock() fails here, @Transactional rolls back // orderRepository.save() — but paymentClient.charge() already fired and // CANNOT be rolled back by this annotation. The customer is now charged // for an order that doesn't exist. }
💻 Code example
@Transactional // this ONLY controls Order Service's own database connection public void placeOrder(OrderRequest request) { orderRepository.save(toOrder(request)); // Order Service's DB — covered by @Transactional paymentClient.charge(request.paymentDetails()); // a REST/gRPC call to a DIFFERENT service's DB — // completely outside this transaction's control inventoryClient.reserveStock(request.items()); // also outside — a network call, not a local operation // if inventoryClient.reserveStock() fails here, @Transactional rolls back // orderRepository.save() — but paymentClient.charge() already fired and // CANNOT be rolled back by this annotation. The customer is now charged // for an order that doesn't exist. }
◆ The problem
Even within just Order Service alone, a subtler version of the same problem exists: "save the order to the database" AND "publish an OrderPlaced event to Kafka" are two separate systems (a database and a message broker) that cannot be committed together in one atomic operation. If the database commit succeeds but the Kafka publish fails (network blip, broker down), the order exists with no event ever published — every downstream consumer (Inventory, Notification) never finds out. This specific, narrower problem is called the dual-write problem, and Chapter 18's Outbox Pattern is built to solve exactly it.
| Chapter | Solves | Trade-off, in one line |
|---|---|---|
| 16 — Two-Phase Commit | Strong, coordinator-driven atomicity across multiple resources | Strongest consistency, worst availability and scalability under failure. |
| 17 — Saga Pattern | A sequence of local transactions with compensating "undo" steps | Highly available and scalable, but only eventually consistent, and needs careful compensation design. |
| 18 — Outbox, Inbox & CDC | The narrower dual-write problem specifically | Solves "database write + event publish" atomically, using infrastructure most teams already run. |
Want a visual for this concept?
Generate a diagram tailored to “Why Local Transactions Fail” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.
Sign in to generate a visual →