The Promise Nobody Can Keep: Two-Phase Commit, Sagas and the End of the Distributed Transaction
🎧 Listen to this article
Software Architecture · 2026-10-02
Fully AI-generated article (no prior review).
The Hook: Two Bookings, One Wish
You are building a travel portal. A customer clicks "Book", and two things are supposed to happen: the flight is reserved, and the hotel room is reserved. Both or neither. A flight without a hotel is annoying; a hotel without a flight is expensive; and a customer who paid for both but received only one is a support case with a long memory.
Inside a single relational database this problem has been solved since the 1970s. You write BEGIN TRANSACTION, execute two INSERTs, write COMMIT, and the database guarantees — by way of its write-ahead log — that either both rows are there or neither, even if the power fails precisely between the two INSERTs. This is one of the most successful abstractions in computer science: you get to program as though nothing could go wrong, because somebody else has already made sure that everything is allowed to go wrong.
But now the flight inventory lives in a different system from the hotel inventory. Two services, two databases, two teams, possibly two data centres. And suddenly the abstraction is gone. The obvious reaction is: then we'll just make the transaction distributed. There is a protocol for that. It is called two-phase commit, it has been known for almost fifty years, it is in every textbook, and in .NET there is even a class with three lines of code around it.
This article is about why that obvious reaction — despite correct theory, despite working implementations — is almost always the wrong one in distributed systems. And why that is not an engineering failure but a mathematically proven property of the world we program in.
The short version: atomicity across system boundaries is not impossible. It is merely expensive — in latency, in lock duration, in availability — and it is blocking, provably so. Anyone who wants it anyway has to buy it with consensus. Anyone unwilling to pay for it has to rewrite the consistency promise rather than break it. Both are legitimate architectures. Only the third option — claiming atomicity while building sagas — is a lie that support eventually pays for.
Part 1: What a Transaction Actually Promises
The transaction concept as we use it today was shaped above all by Jim Gray. In his "Notes on Data Base Operating Systems" (1978, published in Operating Systems, Lecture Notes in Computer Science volume 60, pages 393–481) he describes the transaction management of a database system — locking, logging, recovery — with a completeness that turned the text into the founding document of a discipline. He himself modestly called it "a compendium of data base management operating systems folklore". It is in exactly this work that the two-phase commit protocol is spelled out.
A transaction promises four things, later summarised under the acronym ACID. Two of them matter for our purposes:
Atomicity means the transaction is indivisible. For every observer there are only two states — before and after. An intermediate state in which the flight is booked and the hotel is not does not exist as a visible state of the world.
Isolation means that while the transaction is running, nobody else sees its half-finished effects. Two concurrent transactions behave as though they had run one after the other.
These two promises are the reason transactions are so pleasant — and simultaneously the reason they become so painful across network boundaries. Atomicity demands that all participants reach the same decision. Isolation demands that locks be held until that decision has been made. Locally this takes microseconds. Across a network it takes milliseconds — and sometimes, as we shall see, an unbounded amount of time.
Part 2: Two-Phase Commit, Step by Step
The protocol has two roles: a transaction manager (TM, often called the coordinator) and N resource managers (RMs, the actual databases or services). It proceeds as follows.
Phase 1 — Prepare. The coordinator asks every RM: "Can you commit?" An RM that says yes does something grave: it writes all necessary changes and locks durably into its log and thereby gives a binding promise that it will commit if told to — no matter what happens in between, power failure included. From that moment on it may no longer abort unilaterally. This state is called prepared.
Phase 2 — Decide and announce. Once all RMs have reported "prepared", the coordinator writes the decision COMMIT durably into its own log — that is the moment at which the transaction is irrevocably committed — and announces it to all RMs. If a single RM said "abort" or failed to answer, the decision is ABORT.
Gray and Lamport wrote down the cost of this sequence precisely in 2006: in the normal case 2PC needs "a total of 3N - 1 messages" and "four message delays", plus three forced writes to stable storage — the write by the first RM to prepare, the writes by the remaining RMs when they prepare, and the write by the TM when it makes the commit decision.
Four message delays. Remember that number: with a 1 ms network round trip inside one data centre that is a few milliseconds; across continents it is easily 300 ms. And for the whole of that time, all participating systems are holding their locks.
The Window of Uncertainty
Now comes the part that earned 2PC its bad reputation. Consider an RM that has reported "prepared" and is now waiting for the decision. In this state:
- It may not commit, because another RM might have aborted.
- It may not abort, because it promised that it could commit, and the coordinator may already have decided
COMMITand told others. - It holds its locks, because it must remain able to honour its promise.
This state is called in-doubt. If the coordinator fails at this moment, the RM waits. It waits until the coordinator returns and reads its log. It waits while holding locks. And everything that wants to touch those records waits with it.
This is not an implementation flaw that could be fixed. It is the unavoidable consequence of the RM not being allowed to make the decision itself while the single source of truth is unreachable. In practice it means: a 2PC coordinator is a single point of failure that blocks not merely availability, but the writability of all records involved.
Part 3: The Impossibility You Cannot Optimise Away
This blocking behaviour did not let researchers rest. In 1981 Dale Skeen published "Nonblocking Commit Protocols" at the ACM SIGMOD conference and developed the three-phase commit (3PC): between prepare and commit an additional phase is inserted, a "pre-commit", in which all participants learn that a commit is going to happen before anyone performs it. After this announcement a surviving participant can derive the decision itself without asking the coordinator. Blocking on coordinator failure is thereby eliminated.
Except: it is eliminated under an assumption that does not hold in real systems. 3PC presupposes a network with bounded delay and nodes with bounded response times — a synchronous system model. It also costs an extra message round, and therefore latency.
Two years later Dale Skeen and Michael Stonebraker supplied the formal framework in "A Formal Model of Crash Recovery in a Distributed System" (IEEE Transactions on Software Engineering SE-9(3), May 1983) — together with the result that ended the hope of a friendlier protocol: if the network can partition, that is, if nodes can not only crash but also temporarily fail to reach one another, then there is no commit protocol that is simultaneously consistent and non-blocking. Somebody has to wait. Always.
This is the same kind of result as the FLP theorem in distributed consensus (see How Machines Come to Agree: Distributed Consensus from FLP to Paxos to Raft): not a statement about today's technology, but a boundary of the possible. Anyone who builds a distributed commit protocol that never blocks under partitions and never becomes inconsistent has found either a flaw in the proof or a flaw in their code.
Part 4: The Rescue by Consensus — and Why 2PC Is a Special Case
If the problem is the central coordinator, then the solution is obvious: make the coordinator highly available. That is exactly what Jim Gray and Leslie Lamport did in 2006 in "Consensus on Transaction Commit" (ACM Transactions on Database Systems 31(1), pages 133–160) — one of my favourite papers in the literature, because it unifies two subfields that had existed side by side for twenty years.
The idea: Paxos Commit uses "a separate instance of the Paxos consensus algorithm to obtain agreement on the decision each RM makes of whether to prepare or abort". The decision no longer lives in the log of a single coordinator but is replicated across 2F + 1 coordinators, and it remains able to make progress as long as "at least F + 1 of them are working properly". The failure of up to F coordinators blocks nobody.
And then comes the sentence that clarifies the relationship between the two worlds: "The classic Two-Phase Commit algorithm is obtained as the special F = 0 case of the Paxos Commit algorithm."
Read that twice. 2PC is not a different protocol from consensus. 2PC is consensus — with a replica group of size one. The blocking vulnerability of classic 2PC is not a design flaw; it is precisely the price of not having replicated the coordinator.
The Proof in Practice: Spanner
Google built exactly this recipe. Spanner (Corbett et al., OSDI 2012) executes distributed transactions across multiple shards using two-phase commit over Paxos groups: each participant in the 2PC transaction is not a single server but a Paxos-replicated group with its own leader. The coordinator is such a group as well. This makes the blocking problem all but disappear: a failed coordinator leader is replaced by its group within seconds, and the 2PC log survives in the replicas.
Spanner thereby shows that distributed transactions across data centres are possible. What it shows at the same time is this: to get there you need an infrastructure that puts atomic clocks and GPS receivers into every data centre in order to make time uncertainty explicit (see Clocks That Know Their Own Uncertainty: Google Spanner, TrueTime, and Mastering Time in the Cloud). Distributed atomicity is a database property you buy — not an architectural property you drape over other people's services.
Part 5: The Bill — What Distributed Atomicity Really Costs
Suppose you get the correctness right. What do you pay?
First: latency. Four message delays plus at least two forced log flushes. Inside a rack that is milliseconds; across regions, hundreds. What matters, though, is not the mean but the maximum: the transaction is finished only once the slowest participant has answered. With five participants the 99.99th percentile effectively determines the perceived response time — exactly the mechanism described in The Long Tail of Latency: Tail Latency and Why Averages Lie in the Cloud.
Second: lock duration. All locks are held for the entire duration of the protocol, network latency included. Since throughput under contention scales inversely with lock duration, a tenfold increase in transaction duration under contention roughly means a tenfold decrease in throughput on the hot records.
Third, and most important: availability multiplies. An atomic transaction across n services succeeds only if all n are available. The availabilities multiply:
| Services in one atomic transaction | Availability (99.9 % each) | Downtime per 30-day month |
|---|---|---|
| 1 | 99.900 % | approx. 43 min |
| 2 | 99.800 % | approx. 1 h 26 min |
| 3 | 99.700 % | approx. 2 h 10 min |
| 5 | 99.501 % | approx. 3 h 35 min |
| 10 | 99.004 % | approx. 7 h 10 min |
That is the real punchline. The coupling is not merely a performance question — it is an availability decision. Anyone who forces five services into one atomic transaction has deliberately decided that a booking is impossible whenever any one of five services happens to be restarting. For a bank transfer that may be exactly right. For a hotel reservation it is almost certainly wrong.
Part 6: XA, MSDTC and Practice in .NET
Industry has of course standardised distributed transactions. In 1991 X/Open published the XA specification, which describes the interface between a global transaction manager and a resource manager — that is, it defines what a database or a message broker must be able to do in order to take part in a two-phase commit. The interface derives from the Tuxedo system of the 1980s, and it is implemented to this day in many relational databases and message brokers.
In the .NET ecosystem the tool is called System.Transactions. You write:
using (var scope = new TransactionScope())
{
using (var conn1 = new SqlConnection(dbA)) { conn1.Open(); /* ... */ }
using (var conn2 = new SqlConnection(dbB)) { conn2.Open(); /* ... */ }
scope.Complete();
}
And this genuinely works. Microsoft's documentation describes the mechanism behind it very precisely: as long as only one durable resource is involved, it runs as a "promotable transaction" — a lightweight local SQL Server transaction without coordinator overhead. As soon as a second durable resource enlists, "dynamic escalation" kicks in: System.Transactions brings in the Microsoft Distributed Transaction Coordinator (MSDTC), and from that moment on a real 2PC is running, with everything Part 5 described.
Three details from that documentation are worth remembering, because they regularly surprise people in code reviews:
TransactionScopecreates a transaction with isolation level Serializable by default — the strictest and most lock-intensive. Microsoft explicitly advises lowering it in applications with contention.- The default timeout of a
TransactionScopeis one minute. A transaction that is neither committed nor rolled back can "seriously degrade performance" — which is why theusing-block pattern here is not style but obligation. - In SQL Server CLR contexts,
TransactionScopealways forces promotion, even when used only with the context connection.
And a footnote that says a great deal about the status of distributed transactions in the modern world: in the move from .NET Framework to .NET (Core), MSDTC-based distributed transactions initially disappeared entirely. They were restored only years later — and then exclusively for Windows (dotnet/runtime #715). A cross-platform distributed transaction manager remains an open wish to this day (#71769). I am of the opinion that this is not an oversight but an implicit architectural statement: the platform designed for Linux containers and cloud deployment did not bring the distributed transaction along, because the target architecture no longer calls for it.
The Ugly Corner: Heuristic Decisions
One last detail you should know before putting XA into production. Because in-doubt states hold locks and paralyse operations, many XA implementations offer an emergency exit: an administrator (or a timeout) can unilaterally commit or abort a prepared transaction without the coordinator. This is called a heuristic decision. It solves the operational problem — and in doing so breaks precisely the atomicity the whole protocol was built for. A heuristic abort on one node while the coordinator has committed produces a permanent, silent inconsistency between two databases, each of which is convinced it acted correctly. Anyone running XA needs a procedure for this case — and it is not a technical procedure but a business one.
Part 7: The Counterproposal — Sagas
In 1987, six years after Skeen's 3PC, Hector Garcia-Molina and Kenneth Salem asked an entirely different question at SIGMOD. Their paper is called simply "Sagas" (Proceedings of the 1987 ACM SIGMOD International Conference on Management of Data, pages 249–259). They were not originally concerned with microservices — those did not exist — but with long-lived transactions (LLTs): transactions that run for a long time, a batch job over millions of records for instance, whose locks suffocate the whole system in the meantime.
Their observation has a compelling simplicity: the sequential composition of transactions is not itself a transaction. Break one long transaction into many short ones and you gain throughput — and lose atomicity.
Their answer: replace the promise. A saga is a sequence of transactions T1, T2, …, Tn, each of which commits immediately on its own. For every transaction Ti there exists a compensating transaction Ci that semantically undoes its effect. If T4 fails, the system executes C3, C2, C1 in reverse order. The result is not the exact initial state but — in the paper's own formulation — an acceptable approximation to the state the system had before the saga began. The guarantee is no longer "all or nothing" but "observably equivalent to completely or not at all".
The decisive word is semantically. A compensation is not a rollback. A rollback leaves no trace; a compensation is a new business event.
| Action Ti | Rollback (does not exist) | Compensation Ci (the right thing) |
|---|---|---|
| Hotel room reserved | Reservation never happened | Reservation cancelled, possibly with a cancellation fee |
| Credit card charged | Charge never happened | Refund posted as its own transaction |
| Confirmation email sent | Email unwritten | A second email: "Your booking could not be completed" |
| Inventory decremented | Stock never changed | Stock incremented, reservation released |
The third example is the most instructive: you cannot roll back an email that has been sent. You can only send a second one after it. That is the limit of every compensation architecture — and the reason why, in sagas, the non-compensatable steps are placed at the very end (the "pivot" principle: everything before the pivot is compensatable, everything after it is retriable until it succeeds).
For this the saga needs durable progress storage, the saga log: which step was started, which completed, which compensation is outstanding. If the saga coordinator fails, its successor reads the log and continues — either forward (forward recovery, through to success) or backward (backward recovery, through to full compensation). The fact that this log keeps state as a sequence of events rather than a snapshot, akin to The Logbook of Truth: Understanding Event Sourcing and CQRS, is no coincidence: both patterns rest on being able to reconstruct a sequence in order to continue it.
What Sagas Do Not Give You: Isolation
Here lies the mistake I see most often in architecture discussions. Sagas replace atomicity with compensation. They replace isolation with nothing.
Between T1 and T4 the intermediate state is visible to everyone. The hotel room is reserved, the flight is not yet; if the saga later fails and compensates, somebody may in the meantime have made decisions based on the reserved state. In the jargon: sagas permit dirty reads and lost updates at the business level. The phenomenon is sometimes called a compensation-exposed read, and it is not a bug but the essence of the construction.
The countermeasures are a matter of business design, not technology:
- Semantic lock: the record gets an intermediate status ("reserved, not confirmed") that other readers must respect.
- Commutative updates: formulate operations so that order does not matter —
amount -= 50instead ofamount = 950. The same idea that underlies Agreement Without a Vote: CRDTs and the Art of Conflict-Free Replication. - Pessimistic ordering: push the step with the greatest compensation damage so far back that it hardly ever needs compensating.
- Re-read before acting: before the critical step, check whether the assumptions still hold.
Anyone who builds a saga without having worked through this list has not built a saga; they have built a sequence of database calls with hope.
Part 8: Helland and the End of the Illusion
In 2007 Pat Helland — a man who had worked on exactly these systems at Tandem, Microsoft and Amazon — wrote a position paper for the CIDR conference bearing the finest subtitle in the literature: "Life beyond Distributed Transactions: an Apostate's Opinion". An apostate reporting on his own former faith.
His diagnosis: despite decades of investment in distributed transaction protocols, developers in practice avoid them. He compares them to the Maginot Line — expensively built, and then bypassed. Where projects do attempt them, "the projects founder because the performance costs and fragility make them impractical."
His counterproposal rests on two concepts that recur in every modern microservice or actor architecture.
An entity is a collection of data with a unique key that fits entirely within one scope of serializability — that is, within one partition, on one machine. And then the sentence that governs the whole architecture: "Atomic transactions cannot span entities. The programmer must always stick to the data contained inside a single entity."
Because atomic updates cannot cross entity boundaries, asynchronous messaging becomes the mechanism to coordinate work across entity boundaries. Messages are addressed to entity keys, not physical locations — which lets the system repartition at any time without the business logic noticing. Helland calls this separation scale-agnostic (the upper layer with the business logic) and scale-aware (the lower layer that knows where which entity lives).
The price of this freedom is a property you must build explicitly: because messages can arrive multiple times and out of order during repartitioning, processing must be idempotent. An activity, in Helland's model, is the state an entity keeps per communication partner in order to remember which messages it has already processed.
Helland revisited this theme in 2012 in "Idempotence Is Not a Medical Condition" (ACM Queue 10(4)). The practical consequences for any saga implementation:
- Idempotency keys: every message carries a unique ID; the receiver stores processed IDs in the same local transaction as the business effect and discards duplicates.
- Transactional outbox: you cannot atomically "write to the database and send a message" — that would be a distributed transaction all over again. So you write the business effect and the message to be sent into the same database in one local transaction, and let a separate process drain the outbox table. The result is at-least-once delivery — which is why point 1 is not optional.
- No naked timestamps as a basis for decisions: whether a message is "new" is decided by causal order, not by wall-clock time (see Order Without a Clock: Lamport Timestamps, Vector Clocks, and Causality in Distributed Systems).
The combination "local transaction + outbox + idempotency key + compensation" is, soberly considered, the actual state of the art for business processes that cross service boundaries. It is more work than TransactionScope. In exchange it is also available when one of the services happens to be restarting.
Part 9: The Countermovement — 2PC Is Not Dead
It would be too easy to stop here. The past ten years of research have shown that much of the criticism aimed at 2PC is not really aimed at the protocol but at its environment: a single, unreplicated coordinator, locks held across disk I/O latencies, and networks from the 1980s.
The clearest example is Chardonnay (Tamer Eldeeb, Xincheng Xie, Philip A. Bernstein, Asaf Cidon, Junfeng Yang, OSDI 2023). The system uses classic, lock-based 2PC — and makes it fast, with two ideas.
The dry run. Before the real execution, the transaction runs once "dry", using lock-free snapshot reads. In doing so the system learns which records are needed and loads them into memory. Only then does the real transaction run — and it now holds its locks only for work that happens entirely in RAM. Disk I/O moves out of the contention window.
The epoch service. A replicated, monotonically increasing counter serves as a serialization point; crucially, "the epoch is only read, not incremented, by each transaction" — so it is not a throughput bottleneck.
The numbers are impressive. With fast RPC (eRPC) and NVMe for the write-ahead log, 2PC latency lands at roughly 150 microseconds. And under extreme contention, "Chardonnay's throughput under extremely high contention is only 15% lower than under extremely low contention", where a classic System R*-style system collapses by more than 85 %. On TPC-C New-Order the system scales linearly to more than 200 nodes with stable 3–4 ms 2PC latency at the 99th percentile.
The contrast with Part 5 is instructive. Inside one data centre, with replicated coordinators, an RDMA-class network and NVMe logs, 2PC is an excellent protocol. Across organisational boundaries, between services operated by different teams in different clouds and deployed independently of one another, it is a bad idea — not because the mathematics is different, but because the assumptions are.
I am of the opinion that this is the real lesson: the question "2PC or saga?" is badly posed. The right question is: does the boundary across which I want to stretch a consistency guarantee lie inside or outside a jointly operated domain of trust and latency?
Part 10: A Decision Framework
| Criterion | Distributed transaction (2PC/XA) | Saga with compensation |
|---|---|---|
| Atomicity | Yes (genuine indivisibility) | No, only "observably equivalent" |
| Isolation | Yes (usually Serializable) | No — explicit countermeasures required |
| Availability | Product of all participants | Each service independently available |
| Latency | 4 message delays + log flushes | Sum of the steps, but no global locks |
| Coupling | High, operationally too (deploy, version) | Low, only contract coupling |
| Operational risk | In-doubt states, heuristic decisions | Stuck sagas, faulty compensations |
| Complexity resides | in the infrastructure | in the business logic |
| Fits when | one database/platform, one team, one data centre, hard invariants (money) | service boundaries, third-party systems, long duration, availability over instantaneous consistency |
Three heuristics that have helped me most in practice:
- Move the boundary before you span it. The best distributed transaction is the one that is unnecessary because both records live in the same entity. An aggregate boundary that encloses an invariant saves you an entire protocol. Always check this option first.
- Ask who pays for the inconsistency. For movements of money and regulated bookings the answer is often "an auditor", and then atomicity is worth its price — ideally inside a single database. For reservations, catalogues and notifications the answer is "nobody, if we compensate within seconds".
- Write the compensation first. If you cannot formulate Ci, you have not understood step Ti. And if Ci is impossible, Ti must move backwards — behind the pivot.
The Central Takeaway
Distributed transactions did not fail for lack of theory. The theory is complete: 2PC is correct, it is the F = 0 special case of Paxos Commit, and with replicated coordinators and fast networks it is once again an excellent protocol today. What makes it fail is a property you cannot optimise away: atomicity across n independently operated systems multiplies their failure probabilities and holds locks across network latencies. That is not an implementation question; it is arithmetic.
The practical consequence is uncomfortable but liberating: if you cannot pay for atomicity, do not promise it — rewrite the promise. A saga that honestly says "every step commits immediately, every step has a compensation, the intermediate state is visible in between, and that is why the record carries an intermediate status" is a correct architecture. A chain of service calls that implicitly claims to be atomic is a fault with a delay fuse.
Concretely, if you want to do one thing this week: take a business process in your system that spans two or more services, and write down the compensating action for every step — as a business sentence, not as code. Then mark the pivot: the first step that is no longer compensatable. If that pivot is not at the end, you have just found the most important finding of your quarter.
Cross-References in the Vault
The impossibility result of Skeen and Stonebraker belongs to the same family as FLP, and Paxos Commit makes the kinship explicit: How Machines Come to Agree: Distributed Consensus from FLP to Paxos to Raft. How Google nonetheless makes distributed transactions across continents practical depends directly on making time uncertainty explicit: Clocks That Know Their Own Uncertainty: Google Spanner, TrueTime, and Mastering Time in the Cloud. The saga log is a close relative of the event log: The Logbook of Truth: Understanding Event Sourcing and CQRS. The countermeasure "commutative updates" is the same mathematics that lets replicas converge without coordination: Agreement Without a Vote: CRDTs and the Art of Conflict-Free Replication. Why you cannot identify duplicates by the wall clock is here: Order Without a Clock: Lamport Timestamps, Vector Clocks, and Causality in Distributed Systems. And why, with five participants, the mean response time tells you nothing: The Long Tail of Latency: Tail Latency and Why Averages Lie in the Cloud.
Something to Think About
The Sagas paper replaces "all or nothing" with "an acceptable approximation to the state before the start". That is a business decision, not a technical one — somebody has to decide what acceptable means.
Which consistency promises does your system make today, implicitly, without it being written down anywhere who issued them? And if you had to formulate the compensation for every step of your most important such process — at which point would you notice that you do not know what is supposed to happen when it goes wrong?
Sources
- J. N. Gray, "Notes on Data Base Operating Systems", in: Operating Systems, Lecture Notes in Computer Science 60, Springer 1978, pp. 393–481, DOI 10.1007/3-540-08755-9_9: https://link.springer.com/chapter/10.1007/3-540-08755-9_9
- D. Skeen, "Nonblocking Commit Protocols", Proc. ACM SIGMOD International Conference on Management of Data, 1981, DOI 10.1145/582318.582339: https://dl.acm.org/doi/10.1145/582318.582339
- D. Skeen, M. Stonebraker, "A Formal Model of Crash Recovery in a Distributed System", IEEE Transactions on Software Engineering SE-9(3), May 1983, DOI 10.1109/TSE.1983.236608: https://dl.acm.org/doi/abs/10.1109/TSE.1983.236608
- H. Garcia-Molina, K. Salem, "Sagas", Proceedings of the 1987 ACM SIGMOD International Conference on Management of Data, pp. 249–259, DOI 10.1145/38713.38742: https://dl.acm.org/doi/10.1145/38713.38742
- J. Gray, L. Lamport, "Consensus on Transaction Commit", ACM Transactions on Database Systems 31(1), March 2006, pp. 133–160 (quotations: "a total of 3N - 1 messages", "four message delays", "2F + 1 coordinators", "The classic Two-Phase Commit algorithm is obtained as the special F = 0 case of the Paxos Commit algorithm"): https://lamport.azurewebsites.net/video/consensus-on-transaction-commit.pdf
- P. Helland, "Life beyond Distributed Transactions: an Apostate's Opinion", CIDR 2007 (quotation: "Atomic transactions cannot span entities. The programmer must always stick to the data contained inside a single entity."): https://ics.uci.edu/~cs223/papers/cidr07p15.pdf
- P. Helland, "Idempotence Is Not a Medical Condition", ACM Queue 10(4), 2012, DOI 10.1145/2181796.2187821: https://dl.acm.org/doi/10.1145/2181796.2187821
- J. C. Corbett et al., "Spanner: Google's Globally-Distributed Database", OSDI 2012 (2PC over Paxos groups): https://www.usenix.org/system/files/conference/osdi12/osdi12-final-16.pdf
- T. Eldeeb, X. Xie, P. A. Bernstein, A. Cidon, J. Yang, "Chardonnay: Fast and General Datacenter Transactions for On-Disk Databases", 17th USENIX Symposium on Operating Systems Design and Implementation (OSDI '23), 2023: https://www.usenix.org/system/files/osdi23-eldeeb.pdf
- X/Open, "Distributed Transaction Processing: The XA Specification" (1991), Technical Standard, The Open Group: https://pubs.opengroup.org/onlinepubs/009680699/toc.pdf
- Microsoft Learn, "Distributed transactions" and "System.Transactions integration with SQL Server" (promotable transactions, dynamic escalation to MSDTC, default isolation level Serializable, default timeout of one minute): https://learn.microsoft.com/dotnet/framework/data/adonet/distributed-transactions and https://learn.microsoft.com/sql/connect/ado-net/system-transactions-integration-with-sql-server
- dotnet/runtime, Issue #715 "Implement distributed/promoted transactions in System.Transactions for Windows only" and Issue #71769 "Cross-platform distributed transactions": https://github.com/dotnet/runtime/issues/715 and https://github.com/dotnet/runtime/issues/71769