{"id":"distributed-transactions","title":"Distributed transactions and sagas","sections":[{"id":"overview","title":"Overview","html":"<p>Inside one relational database, a transaction makes several writes succeed or fail together. The primer's <a href=\"/learn/database#relational-database-management-system-rdbms\">RDBMS section</a> lists the ACID properties that make this work. Split the system into <a href=\"/learn/application-layer#microservices\">microservices</a>, each with its own database, and that guarantee is gone. Placing an order might touch the order service, the inventory service and the payment service. If the payment fails after inventory was reserved, nothing rolls the reservation back for you.</p>\n<p>This chapter covers the two families of answers. <strong>Two-phase commit</strong> tries to keep atomicity across databases, at a real cost in availability. <strong>Sagas</strong> give up atomic isolation and instead run a sequence of local transactions with compensating steps to undo work. It also covers the <strong>dual-write problem</strong>, the subtle bug where a service updates its database and publishes an event, and only one of the two happens. Interviewers bring these up in any design that involves money, bookings or inventory.</p>","headings":[]},{"id":"why-one-transaction-cant-span-services","title":"Why one transaction can't span services","html":"<h3 id=\"why-one-transaction-cant-span-services\">Why one transaction can't span services</h3>\n<p>A local transaction relies on a single database engine holding locks, writing one log, and deciding commit or abort in one place. Across services, none of that exists:</p>\n<ul>\n<li><strong>Each service owns its data.</strong> Other services reach it only through its API, so there is no shared engine to hold locks across all of it.</li>\n<li><strong>The stores may differ.</strong> The order service might use Postgres, inventory might use DynamoDB, and payment goes through an external card processor that has no notion of your transaction.</li>\n<li><strong>Locks across the network are expensive.</strong> Holding a row lock while you wait for another service to reply makes every concurrent request for that row wait as well.</li>\n<li><strong>Availability multiplies.</strong> If a commit needs all participants to be up, the components act <a href=\"/learn/availability-patterns#availability-in-numbers\">in sequence</a>. Three services at 99.9% each give 0.999 × 0.999 × 0.999 ≈ 0.997, or 99.7%, for the operation as a whole.</li>\n</ul>","headings":[{"id":"why-one-transaction-cant-span-services","title":"Why one transaction can't span services","depth":3}]},{"id":"two-phase-commit-and-its-blocking-problem","title":"Two-phase commit and its blocking problem","html":"<h3 id=\"two-phase-commit-and-its-blocking-problem\">Two-phase commit and its blocking problem</h3>\n<p><strong>Two-phase commit</strong> (2PC) is the classic protocol for atomic commits across several databases. One node acts as <strong>coordinator</strong>; the databases are <strong>participants</strong>.</p>\n<ol>\n<li><strong>Prepare.</strong> The coordinator asks every participant whether it can commit. Each participant does the work, writes it durably along with a \"prepared\" record, keeps its locks, and votes yes or no. A yes vote is a promise: the participant can no longer abort on its own.</li>\n<li><strong>Commit or abort.</strong> If all votes are yes, the coordinator durably records \"commit\" and tells everyone to commit. If any vote is no, or a participant doesn't answer in time, it tells everyone to abort.</li>\n</ol>\n<p>The weak point is the gap between the phases. Suppose all participants vote yes and the coordinator crashes before sending the decision. The participants are now <strong>in doubt</strong>. They can't commit, because another participant might have voted no. They can't abort, because the coordinator might have decided commit and told someone else. So they wait, holding their locks, until the coordinator recovers. Any transaction that needs those rows waits too. This is why 2PC is called a <strong>blocking</strong> protocol.</p>\n<p>2PC is still used where its costs are acceptable. Databases support it through XA or commands like PostgreSQL's <code>PREPARE TRANSACTION</code> (disabled by default, since <code>max_prepared_transactions</code> defaults to 0). Google Spanner runs 2PC across shards, but each participant and the coordinator are themselves replicated with Paxos, so a single machine failure doesn't leave anyone in doubt. That is the general fix: make the coordinator highly available using <a href=\"/learn/consensus\">consensus</a>. It works, but it is a database-internal technique. Across independently owned services, especially external ones like a payment provider, 2PC is rarely an option.</p>","headings":[{"id":"two-phase-commit-and-its-blocking-problem","title":"Two-phase commit and its blocking problem","depth":3}]},{"id":"sagas","title":"Sagas","html":"<h3 id=\"sagas\">Sagas</h3>\n<p>A <strong>saga</strong> is a sequence of local transactions, each in one service. Each step commits on its own and triggers the next. If a step fails, the saga runs <strong>compensating transactions</strong> for the steps that already committed, in reverse order. The idea comes from a 1987 paper by Hector Garcia-Molina and Kenneth Salem on long-lived database transactions.</p>\n<h4 id=\"example-booking-concert-tickets\">Example: booking concert tickets</h4>\n<p>A ticketing site sells reserved seats. Buying two seats involves four services. Here is one saga for it, with each step's compensation:</p>\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n<div class=\"table-wrap\"><table><thead><tr><th>Step</th><th>Service</th><th>Action</th><th>Compensation</th></tr></thead><tbody><tr><td>1</td><td>Orders</td><td>Create order with status <code>PENDING</code></td><td>Mark order <code>CANCELLED</code></td></tr><tr><td>2</td><td>Seats</td><td>Place a 10-minute hold on seats 14A and 14B</td><td>Release the hold</td></tr><tr><td>3</td><td>Payments</td><td>Authorize $180 on the card</td><td>Void the authorization</td></tr><tr><td>4</td><td>Payments</td><td>Capture the $180</td><td>None: this is the point of no return</td></tr><tr><td>5</td><td>Tickets</td><td>Issue tickets and email them</td><td>None: retried until it succeeds</td></tr><tr><td>6</td><td>Orders</td><td>Mark order <code>CONFIRMED</code></td><td>None: retried until it succeeds</td></tr></tbody></table></div>\n<p>The order of the steps is deliberate.</p>\n<ul>\n<li><strong>Cheap, invisible steps go first.</strong> Releasing a seat hold costs nothing. Voiding an authorization doesn't show up as a charge on the customer's statement. If the card is declined at step 3, undoing steps 1 and 2 is harmless.</li>\n<li><strong>One step is the pivot.</strong> Capturing the payment (step 4) is the step after which the saga commits to finishing. Before it, everything can be compensated. After it, nothing is undone.</li>\n<li><strong>Steps after the pivot must be retriable.</strong> Issuing tickets can fail temporarily, but it must eventually succeed, so it is retried with backoff rather than compensated. These steps must be idempotent, because retries will repeat them (see <a href=\"/learn/reliability-patterns\">reliability patterns</a>).</li>\n</ul>\n<p>Compensation is not the same as rollback. A rollback makes it as if the transaction never happened. A compensation is a new action that semantically reverses an old one, and other parts of the system may have seen the intermediate state. If a confirmation email has already gone out, the compensation is a second email saying \"your order was cancelled\", not unsending the first. Compensations also have to be idempotent and must not fail for business reasons. \"Release the hold\" has to work even if the hold already expired.</p>\n<h4 id=\"orchestration-and-choreography\">Orchestration and choreography</h4>\n<p>There are two ways to drive a saga.</p>\n<p>In <strong>orchestration</strong>, a single orchestrator (often part of the order service, or a workflow engine such as AWS Step Functions or Temporal) tells each service what to do, waits for the reply, and decides the next step or the compensations. It persists the saga's state, so it can resume after a crash.</p>\n<figure class=\"diagram\" data-text=\"Buyer -> Order saga orchestrator (place order); Order saga orchestrator -> Saga state; Order saga orchestrator -> Seat service (hold / release); Order saga orchestrator -> Payment service (auth, void, capture); Order saga orchestrator -> Ticket service (issue)\"><div class=\"diagram-scroll\"><svg class=\"dg\" viewBox=\"0 0 720 303.8\" width=\"720\" role=\"img\" aria-label=\"An orchestrator drives each step and its compensation, and persists saga state\"><defs><marker id=\"dg-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path class=\"dg-arrowhead\" d=\"M0,1 L9,5 L0,9 z\"></path></marker></defs><line class=\"dg-edge\" x1=\"151\" y1=\"138\" x2=\"247\" y2=\"138\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"320\" y1=\"167\" x2=\"320\" y2=\"228.8\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge dg-dashed\" x1=\"391\" y1=\"116.7\" x2=\"567\" y2=\"63.9\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge dg-dashed\" x1=\"391\" y1=\"138\" x2=\"567\" y2=\"138\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge dg-dashed\" x1=\"391\" y1=\"159.3\" x2=\"567\" y2=\"212.1\" marker-end=\"url(#dg-arrow)\"></line><rect class=\"dg-node dg-client\" x=\"12\" y=\"114\" width=\"136\" height=\"48\" rx=\"24\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"80\" y=\"142.5\">Buyer</tspan></text><rect class=\"dg-node dg-box\" x=\"252\" y=\"112\" width=\"136\" height=\"52\" rx=\"8\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"320\" y=\"134.5\">Order saga</tspan><tspan x=\"320\" y=\"150.5\">orchestrator</tspan></text><path class=\"dg-node dg-store\" d=\"M252,240.79999999999995 a68,7 0 0,1 136,0 v44 a68,7 0 0,1 -136,0 z\"></path><path class=\"dg-line\" d=\"M252,240.79999999999995 a68,7 0 0,0 136,0\"></path><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"320\" y=\"270.3\">Saga state</tspan></text><rect class=\"dg-node dg-box\" x=\"572\" y=\"18\" width=\"136\" height=\"48\" rx=\"8\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"640\" y=\"46.5\">Seat service</tspan></text><rect class=\"dg-node dg-box\" x=\"572\" y=\"114\" width=\"136\" height=\"48\" rx=\"8\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"640\" y=\"142.5\">Payment service</tspan></text><rect class=\"dg-node dg-box\" x=\"572\" y=\"210\" width=\"136\" height=\"48\" rx=\"8\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"640\" y=\"238.5\">Ticket service</tspan></text><rect class=\"dg-edge-bg\" x=\"159.9\" y=\"129\" width=\"78.2\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"199\" y=\"142\" text-anchor=\"middle\">place order</text><rect class=\"dg-edge-bg\" x=\"430.6\" y=\"81.3\" width=\"96.8\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"479\" y=\"94.3\" text-anchor=\"middle\">hold / release</text><rect class=\"dg-edge-bg\" x=\"415.1\" y=\"129\" width=\"127.8\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"479\" y=\"142\" text-anchor=\"middle\">auth, void, capture</text><rect class=\"dg-edge-bg\" x=\"458.5\" y=\"176.7\" width=\"41\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"479\" y=\"189.7\" text-anchor=\"middle\">issue</text></svg></div><figcaption>An orchestrator drives each step and its compensation, and persists saga state</figcaption></figure>\n<p>In <strong>choreography</strong>, there is no central controller. Each service listens for events and reacts: the seat service sees <code>OrderCreated</code> and places a hold, then emits <code>SeatsHeld</code>; the payment service sees <code>SeatsHeld</code> and authorizes, and so on. Failures are events too (<code>PaymentDeclined</code>), and each service knows which events should make it compensate.</p>\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n<div class=\"table-wrap\"><table><thead><tr><th></th><th>Orchestration</th><th>Choreography</th></tr></thead><tbody><tr><td>Where the flow lives</td><td>In one place, readable as code</td><td>Spread across services' event handlers</td></tr><tr><td>Coupling</td><td>Orchestrator knows every participant</td><td>Services know only the events they consume</td></tr><tr><td>Debugging \"where is order 991?\"</td><td>Ask the orchestrator</td><td>Reconstruct from events in several services</td></tr><tr><td>Risk</td><td>Orchestrator grows into a god service</td><td>Cyclic dependencies, flows nobody can see whole</td></tr><tr><td>Good fit</td><td>Many steps, branching, compensations</td><td>Short flows with two or three steps</td></tr></tbody></table></div>\n<p>A common path is to start with choreography and move to orchestration once a flow grows past a few steps or needs careful compensation logic.</p>\n<h4 id=\"the-missing-isolation\">The missing isolation</h4>\n<p>A saga has atomicity in a loose sense (it eventually completes or compensates) but no isolation. Other requests can see and act on intermediate states. Two examples from the ticket saga: a reporting job counts seats 14A and 14B as sold while they are only held, and the buyer clicks \"cancel order\" while the payment step is still running.</p>\n<p>Common countermeasures:</p>\n<ul>\n<li><strong>Semantic locks.</strong> Make intermediate states explicit in the data. The order is <code>PENDING</code>, the seats are <code>HELD</code> rather than <code>SOLD</code>. Other operations check these flags: reports count only <code>SOLD</code> seats, and a cancel request on a <code>PENDING</code> order is rejected or queued until the saga settles.</li>\n<li><strong>Order the steps to limit exposure.</strong> As in the table above: reversible steps before the pivot, retriable steps after it.</li>\n<li><strong>Reread before you write.</strong> A step checks that the record hasn't changed since the saga started, for example by comparing a version number, and fails (triggering compensation) if it has.</li>\n<li><strong>Prefer commutative updates.</strong> \"Add 2 to held count\" and \"subtract 2\" can be applied in any order, unlike \"set held count to 7\".</li>\n</ul>","headings":[{"id":"sagas","title":"Sagas","depth":3},{"id":"example-booking-concert-tickets","title":"Example: booking concert tickets","depth":4},{"id":"orchestration-and-choreography","title":"Orchestration and choreography","depth":4},{"id":"the-missing-isolation","title":"The missing isolation","depth":4}]},{"id":"the-dual-write-problem-and-the-transactional-outbox","title":"The dual-write problem and the transactional outbox","html":"<h3 id=\"the-dual-write-problem-and-the-transactional-outbox\">The dual-write problem and the transactional outbox</h3>\n<p>Saga steps are usually connected by events. That introduces a subtle bug. The order service writes the new order to its database and then publishes <code>OrderCreated</code> to Kafka. If it crashes between the two, the order exists but no one hears about it. Reverse the order, and a crash can publish an event for an order that was never saved. Two separate systems can't be updated atomically without something like 2PC, which message brokers and databases rarely share. This is the <strong>dual-write problem</strong>, and it appears anywhere a service updates a database and a cache, search index or broker separately.</p>\n<p>The <strong>transactional outbox</strong> avoids it by writing only to the database. In the same local transaction as the business change, the service inserts the event into an <code>outbox</code> table. Both rows commit or neither does. A separate <strong>relay</strong> then reads the outbox and publishes each event to the broker.</p>\n<figure class=\"diagram\" data-text=\"Order service -> Orders DB: orders + outbox (one local txn); Orders DB: orders + outbox -> CDC relay (e.g. Debezium) (read WAL); CDC relay (e.g. Debezium) -> order-events topic (publish); order-events topic -> Seat and payment services\"><div class=\"diagram-scroll\"><svg class=\"dg\" viewBox=\"0 0 800 204.8\" width=\"800\" role=\"img\" aria-label=\"The order row and its event commit together; a relay publishes the event later\"><defs><marker id=\"dg-arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path class=\"dg-arrowhead\" d=\"M0,1 L9,5 L0,9 z\"></path></marker></defs><line class=\"dg-edge\" x1=\"151\" y1=\"42\" x2=\"327\" y2=\"42\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge\" x1=\"400\" y1=\"76\" x2=\"400\" y2=\"135.8\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge dg-dashed\" x1=\"471\" y1=\"166.8\" x2=\"647\" y2=\"166.8\" marker-end=\"url(#dg-arrow)\"></line><line class=\"dg-edge dg-dashed\" x1=\"720\" y1=\"137.8\" x2=\"720\" y2=\"73\" marker-end=\"url(#dg-arrow)\"></line><rect class=\"dg-node dg-box\" x=\"12\" y=\"18\" width=\"136\" height=\"48\" rx=\"8\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"80\" y=\"46.5\">Order service</tspan></text><path class=\"dg-node dg-store\" d=\"M332,18 a68,7 0 0,1 136,0 v48 a68,7 0 0,1 -136,0 z\"></path><path class=\"dg-line\" d=\"M332,18 a68,7 0 0,0 136,0\"></path><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"400\" y=\"41.5\">Orders DB:</tspan><tspan x=\"400\" y=\"57.5\">orders + outbox</tspan></text><rect class=\"dg-node dg-box\" x=\"332\" y=\"140.8\" width=\"136\" height=\"52\" rx=\"8\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"400\" y=\"163.3\">CDC relay</tspan><tspan x=\"400\" y=\"179.3\">(e.g. Debezium)</tspan></text><rect class=\"dg-node dg-queue\" x=\"652\" y=\"140.8\" width=\"136\" height=\"52\" rx=\"6\"></rect><line class=\"dg-line\" x1=\"772\" y1=\"148.8\" x2=\"772\" y2=\"184.8\"></line><line class=\"dg-line\" x1=\"762\" y1=\"148.8\" x2=\"762\" y2=\"184.8\"></line><line class=\"dg-line\" x1=\"752\" y1=\"148.8\" x2=\"752\" y2=\"184.8\"></line><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"706\" y=\"163.3\">order-events</tspan><tspan x=\"706\" y=\"179.3\">topic</tspan></text><rect class=\"dg-node dg-box\" x=\"652\" y=\"16\" width=\"136\" height=\"52\" rx=\"8\"></rect><text class=\"dg-label\" text-anchor=\"middle\"><tspan x=\"720\" y=\"38.5\">Seat and payment</tspan><tspan x=\"720\" y=\"54.5\">services</tspan></text><rect class=\"dg-edge-bg\" x=\"193.7\" y=\"33\" width=\"90.6\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"239\" y=\"46\" text-anchor=\"middle\">one local txn</text><rect class=\"dg-edge-bg\" x=\"370.2\" y=\"96.9\" width=\"59.6\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"400\" y=\"109.9\" text-anchor=\"middle\">read WAL</text><rect class=\"dg-edge-bg\" x=\"532.3\" y=\"157.8\" width=\"53.4\" height=\"18\" rx=\"4\"></rect><text class=\"dg-edge-label\" x=\"559\" y=\"170.8\" text-anchor=\"middle\">publish</text></svg></div><figcaption>The order row and its event commit together; a relay publishes the event later</figcaption></figure>\n<p>The relay comes in two forms:</p>\n<ul>\n<li><strong>Polling publisher.</strong> Query the outbox for unpublished rows, publish them, mark them sent. Simple, but polling adds latency and load, and under concurrent writers rows can become visible in a different order than their IDs suggest.</li>\n<li><strong>Change data capture (CDC).</strong> Read the database's replication log (PostgreSQL's write-ahead log via logical decoding, MySQL's binlog) and turn committed outbox inserts into messages. Debezium is a common open-source tool for this, and has an outbox-specific router. CDC delivers events in commit order with low latency, at the cost of running another piece of infrastructure.</li>\n</ul>\n<p>Either way, the relay may publish an event more than once (it can crash after publishing but before recording progress), so consumers must deduplicate by event ID. Key the topic by the aggregate ID, such as the order ID, so a given order's events stay in order (see <a href=\"/learn/event-streaming\">event streaming</a>). Delete or archive outbox rows after publishing so the table doesn't grow forever.</p>\n<p>You might ask why not use CDC on the <code>orders</code> table itself and skip the outbox. You can, but then consumers depend on your internal table layout, and every schema change risks breaking them. The outbox gives you an explicit event format that you control.</p>","headings":[{"id":"the-dual-write-problem-and-the-transactional-outbox","title":"The dual-write problem and the transactional outbox","depth":3}]},{"id":"check-yourself","title":"Check yourself","html":"<h3 id=\"check-yourself\">Check yourself</h3>\n<details>\n<summary>In the ticket saga, why authorize the card before capturing it, instead of charging it in one step?</summary>\n<p>Authorization reserves the money without moving it, and voiding it is cheap and nearly invisible to the customer. That keeps the payment step compensatable while the seat hold is still uncertain. Capture becomes the pivot, done only once everything before it has succeeded, so you avoid charging and then refunding, which costs fees and alarms customers.</p>\n</details>\n<details>\n<summary>A 2PC participant voted yes and then lost contact with the coordinator. Why can't it just time out and abort?</summary>\n<p>Its yes vote promised it would commit if told to. The coordinator may already have decided commit and told other participants, who have committed. If this participant aborted, the transaction would be committed in some places and not others, which is exactly what 2PC exists to prevent. So it must keep its locks and wait to learn the decision.</p>\n</details>\n<details>\n<summary>Your user service updates a user's email in Postgres and then calls the search service's API to update the index. Sometimes search shows the old email for days. What's wrong, and how would you fix it?</summary>\n<p>It's a dual write: when the second call fails or the process crashes between the two, the index never gets the update and nothing retries. Write an event to an outbox table in the same transaction as the email change, and have a relay or CDC pipeline deliver it to the search indexer, which applies it idempotently. The index will lag by seconds, but it will converge.</p>\n</details>\n<details>\n<summary>A customer cancels an order while its saga is between \"seats held\" and \"payment captured\". How should the system handle this?</summary>\n<p>The order is in a <code>PENDING</code> state, which acts as a semantic lock. The simplest safe approach is to record the cancel request and let the saga act on it: if the saga hasn't reached the pivot, it runs compensations (void the authorization, release the seats); if it has, the cancel becomes a normal post-purchase refund flow. What you must avoid is a cancel handler that releases seats directly while the saga is still capturing payment.</p>\n</details>\n<details>\n<summary>A teammate proposes choreography for a 9-step insurance claim workflow with several branches and manual review. What would you push back on?</summary>\n<p>With nine steps and branches, the flow would be spread across many services' event handlers, and nobody could see or change it in one place. Answering \"why is claim 123 stuck?\" would mean piecing events together from several services. An orchestrator, or a workflow engine, keeps the steps, branches, timeouts and compensations in one readable definition, at the cost of a central component you have to keep available.</p>\n</details>\n<h3 id=\"further-reading\">Further reading</h3>\n<ul>\n<li><a href=\"https://www.cs.cornell.edu/andru/cs711/2002fa/reading/sagas.pdf\" target=\"_blank\" rel=\"noopener noreferrer\">Hector Garcia-Molina and Kenneth Salem, \"Sagas\" (1987)</a>: the original paper.</li>\n<li><a href=\"https://microservices.io/patterns/data/saga.html\" target=\"_blank\" rel=\"noopener noreferrer\">Pattern: Saga</a> and <a href=\"https://microservices.io/patterns/data/transactional-outbox.html\" target=\"_blank\" rel=\"noopener noreferrer\">Pattern: Transactional outbox</a> by Chris Richardson.</li>\n<li><a href=\"https://queue.acm.org/detail.cfm?id=3025012\" target=\"_blank\" rel=\"noopener noreferrer\">Pat Helland, \"Life beyond Distributed Transactions\"</a> (ACM Queue): designing for a world without distributed transactions.</li>\n<li><a href=\"https://www.postgresql.org/docs/current/sql-prepare-transaction.html\" target=\"_blank\" rel=\"noopener noreferrer\">PostgreSQL: PREPARE TRANSACTION</a>: two-phase commit in a real database, and its warnings.</li>\n<li><a href=\"https://debezium.io/documentation/reference/stable/transformations/outbox-event-router.html\" target=\"_blank\" rel=\"noopener noreferrer\">Debezium outbox event router</a>: an outbox relay built on CDC.</li>\n<li>Alex Xu, <em>System Design Interview – An Insider's Guide, Volume 2</em> (2022), with Sahn Lam, \"Digital wallet\".</li>\n<li><a href=\"https://systemdesignschool.io/primer\" target=\"_blank\" rel=\"noopener noreferrer\">System Design School primer</a>: \"Multi-Step Workflows and Sagas\".</li>\n</ul>","headings":[{"id":"check-yourself","title":"Check yourself","depth":3},{"id":"further-reading","title":"Further reading","depth":3}]}],"sourceFiles":[]}