Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Database Management Systems (DBMS)  Module 15 – Distributed and cloud databases

Distributed transactions: 2PC, consensus, sagas

Run two-phase commit and see where it blocks, learn why Paxos and Raft do not replace it, and design sagas with compensations and an outbox.

  • Advanced
  • 40 minutes
  • Examples run with sql.js 1.14.2, Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free core, Pro depth: 13 of 40 minutes free
Free: Two-phase commit, step by step · Where two-phase commit blocks · Consensus is not two-phase commit · 3 more sections. With a pass: 3 in-depth parts and 3 interview answers, about 27 minutes.

What you will learn

  • Run two-phase commit and identify where it blocks
  • Explain how consensus (Paxos, Raft) replicates a log and why it does not replace 2PC
  • Design a saga with compensating actions and an outbox for cross-service workflows

Before you start

A distributed transaction changes data on more than one server and must take effect on all of them or on none. Two-phase commit (2PC) does it with a coordinator: first every participant does its work, records that it is prepared and votes; then the coordinator logs the decision, commit only if every vote was yes, and tells the participants. Its weakness is waiting: a participant that has voted yes may not decide alone, so if the coordinator fails before telling it, the participant keeps its locks until the coordinator returns. Consensus protocols such as Paxos and Raft solve a different problem: they keep the replicas of one log in agreement while a minority of them fails. They keep each shard available but cannot make two shards commit together, so systems that need both combine them. A saga gives up atomic commit altogether: one local transaction per service, and compensating actions that undo the earlier steps when a later one fails.

Two-phase commit, step by step

Two-phase commit: the coordinator collects two yes votes, logs commit, then tells both participants, which release their locks.ACoordinatorB1. prepare1. prepare2. yes(prepared,locks held)2. yes(prepared,locks held)3. log commit(commit point)4. commit4. commit5. done,locks freed5. done,locks freed

Two-phase commit with a coordinator and two participants

Text description of the diagram

A sequence diagram with three lifelines: participant A, the coordinator and participant B.

1. The coordinator sends prepare to A and to B. 2. Each participant does its work, writes a prepare record to its own log, keeps its locks and votes yes. 3. With both votes in, the coordinator writes commit to its log. This record is the commit point: from here on the transaction will commit, whatever fails. 4. The coordinator sends commit to A and to B. 5. Each participant commits, releases its locks and answers done.

If any participant had voted no, or no vote had arrived in time, the coordinator would log abort instead and send abort in step 4.

In the first phase, each participant runs its part of the transaction, writes a prepare record to its own log and only then votes yes, a promise that it can commit even after a crash. In the second phase the coordinator writes its decision to its log. That record is the commit point: before it the transaction can still be aborted, after it the transaction will commit, whatever fails. The coordinator then sends the decision, and each participant commits or aborts and releases its locks. One no vote, or a vote that does not arrive in time, makes the decision abort. A coordinator that presumes abort reads a missing decision in its log as abort.

Where two-phase commit blocks

The simulator below runs one transaction with two participants, A and B, and injects one crash at each step of the protocol. Each crash lasts 30 seconds, and every party’s log survives it:

Two-phase commit with a crash at every step Python · python/two_pc_sim.py
"""Two-phase commit between a coordinator and two participants, A and B, with one crash injected at each step.

Each party keeps a log that survives its crash. A participant that has voted yes may not decide alone: it waits for
the decision, and its locks stay held while it waits. The coordinator presumes abort: after a crash, a transaction
with no decision in its log is aborted. Every crash here lasts 30 seconds."""

CRASH_SECONDS = 30


class Participant:
    def __init__(self, name, vote="yes"):
        self.name, self.vote, self.log, self.state = name, vote, [], "working"

    def prepare(self):
        if self.vote == "yes":
            self.log.append("prepared")  # forced to disk before the vote is sent
            self.state = "prepared"
        else:
            self.state = "aborted"
        return self.vote

    def decide(self, decision):
        self.log.append(decision)
        self.state = "committed" if decision == "commit" else "aborted"


def run(crash):
    """Runs one transaction; `crash` names who crashes and where. Returns the outcome and who waited."""
    a, b = Participant("A"), Participant("B", vote="no" if crash == "B votes no" else "yes")
    coordinator_log = []
    waited = []
    if crash == "coordinator, before sending prepare":
        # Nobody has voted, so the participants time out and abort on their own.
        for p in (a, b):
            p.decide("abort")
        return a, b, waited
    votes = []
    for p in (a, b):
        if crash == "A, before voting" and p is a:
            votes.append("none")  # the coordinator times out waiting for A
            continue
        votes.append(p.prepare())
    decision = "commit" if votes == ["yes", "yes"] else "abort"
    if crash == "coordinator, after the votes, before logging":
        waited = [p.name for p in (a, b) if p.state == "prepared"]
        decision = "commit" if ("commit" in coordinator_log) else "abort"  # presumed abort on recovery
        for p in (a, b):
            if p.state == "prepared":
                p.decide(decision)
        return a, b, waited
    coordinator_log.append(decision)  # the commit point
    if crash == "coordinator, after logging, before sending":
        waited = [p.name for p in (a, b) if p.state == "prepared"]
        for p in (a, b):
            if p.state == "prepared":
                p.decide(coordinator_log[-1])  # sent once the coordinator is back
        return a, b, waited
    for p in (a, b):
        if crash == "coordinator, after telling A only" and p is b:
            waited = ["B"]
            continue
        if p.state == "prepared":
            if crash == "A, after voting yes" and p is a:
                waited = ["A"]  # down for 30 s, then asks the coordinator, which answers from its log
            p.decide(decision)
    if crash == "coordinator, after telling A only":
        b.decide(coordinator_log[-1])
    if crash == "A, before voting":
        a.decide("abort")  # A comes back with no prepare record, so it aborts
    return a, b, waited


SCENARIOS = [
    "no crash",
    "B votes no",
    "coordinator, before sending prepare",
    "A, before voting",
    "A, after voting yes",
    "coordinator, after the votes, before logging",
    "coordinator, after logging, before sending",
    "coordinator, after telling A only",
]

print(f"{'crash':<46} {'A':<10} {'B':<10} prepared, holding locks for {CRASH_SECONDS} s")
for crash in SCENARIOS:
    a, b, waited = run(crash)
    assert a.state == b.state, "atomicity broken"
    print(f"{crash:<46} {a.state:<10} {b.state:<10} {', '.join(waited) or '-'}")
print()
print("A and B ended in the same state every time: commit or abort is all or nothing.")
print("Every wait comes after a yes vote, while the decision was out of reach of the participant.")

Output

crash                                          A          B          prepared, holding locks for 30 s
no crash                                       committed  committed  -
B votes no                                     aborted    aborted    -
coordinator, before sending prepare            aborted    aborted    -
A, before voting                               aborted    aborted    -
A, after voting yes                            committed  committed  A
coordinator, after the votes, before logging   aborted    aborted    A, B
coordinator, after logging, before sending     committed  committed  A, B
coordinator, after telling A only              committed  committed  B

A and B ended in the same state every time: commit or abort is all or nothing.
Every wait comes after a yes vote, while the decision was out of reach of the participant.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 two_pc_sim.py

A and B ended in the same state every time, so atomicity held. What differs is the waiting. Before a participant has voted yes it may still abort on its own, which is why the early crashes end in a quick abort. After a yes vote it may not: the coordinator might already have logged a commit. So in the three crashes of the coordinator after the votes, the prepared participants waited out the whole crash with their locks held. When the coordinator had told A only, B could also have asked A for the outcome; that only works while another participant is reachable and knows it. PostgreSQL implements the participant’s side with PREPARE TRANSACTION, COMMIT PREPARED and ROLLBACK PREPARED, and ships with the feature turned off: max_prepared_transactions is 0 by default (PostgreSQL 18 manual, 19.4).

Consensus is not two-phase commit

Paxos lets a set of acceptors choose one value even when some of them fail, and accepts a value once a majority has accepted it; any two majorities share a member, so two different values can never both be chosen (Lamport). Raft builds a replicated log on the same idea: an elected leader appends entries, and an entry is committed once a majority of servers store it (Ongaro and Ousterhout). Five servers keep working with two of them down. Two-phase commit needs something else. It needs every participant’s yes, because each participant checks its own data and may refuse: a balance that would go negative on one shard must stop the transfer on the other. A majority cannot outvote that refusal. What consensus can fix is the waiting: if the coordinator’s log is itself replicated by consensus, a coordinator crash no longer hides the decision, since another replica of the coordinator takes over. Google’s Spanner is built that way: two-phase commit runs between groups that are each replicated by Paxos (Corbett et al.).

Not affiliated

Based on public information about Google. MySmartCoPilot is not affiliated with Google, and Google’s real systems may differ from what this lesson describes.

Sagas and the outbox

A saga is a sequence of local transactions, each with a compensating transaction that undoes it in the business sense; the system runs all the steps, or runs some and then their compensations in reverse order (Garcia-Molina and Salem). Nothing is held between steps, so other transactions see a saga’s intermediate states, and the design must allow for that. Services then need a reliable way to send each other the next step. An outbox is a table in the service’s own database: the message is written in the same local transaction as the change it announces, and a relay publishes it afterwards. The relay may publish a message twice, so each consumer keeps an inbox of the messages it has handled and applies each one once.

Exercise

Exercise · Easy · Python

Decide, wait or recover in two-phase commit

Write three functions in two_phase.py for a coordinator that presumes abort.

  • decide(votes) returns "commit" when every participant voted "yes", and "abort" otherwise. votes maps a participant's name to "yes", "no" or None (no vote arrived before the time-out).
  • may_abort_alone(state) returns True when a participant in state may abort on its own because the coordinator is silent. The states are "working" (not asked to vote yet, or still working), "prepared" (voted yes), "committed" and "aborted".
  • recover(log, gid) returns what a restarted coordinator tells a participant that asks about transaction gid: the decision its log holds, or "abort" when the log holds none. The log is a list of (gid, decision) records in the order they were written.

The sample tests include a participant that already knows the outcome, and a log that holds several transactions.

Starter code · two_phase.py

"""Decisions in two-phase commit with presumed abort."""


def decide(votes):
    """'commit' when every vote is 'yes', else 'abort' (a missing vote is None)."""
    # Replace this line with your code.
    return None


def may_abort_alone(state):
    """True when a participant in this state may abort on its own while the coordinator is silent."""
    # Replace this line with your code.
    return None


def recover(log, gid):
    """The decision a restarted coordinator gives for gid: from its log, or 'abort' when none is logged."""
    # Replace this line with your code.
    return None
The sample tests · test_two_phase.py
from two_phase import decide, may_abort_alone, recover


def test_commit_needs_every_yes():
    """one no, or one missing vote, aborts the transaction"""
    assert decide({"A": "yes", "B": "yes"}) == "commit"
    assert decide({"A": "yes", "B": "no"}) == "abort"
    assert decide({"A": "yes", "B": None}) == "abort"
    assert decide({"A": "yes", "B": "yes", "C": "yes"}) == "commit"


def test_only_an_unprepared_participant_may_abort_alone():
    """a yes vote gives the decision away; an outcome already known needs no decision"""
    assert may_abort_alone("working") is True
    assert may_abort_alone("prepared") is False
    assert may_abort_alone("committed") is False
    assert may_abort_alone("aborted") is False


def test_recovery_reads_the_log():
    """the logged decision of that transaction is the answer"""
    log = [("t1", "commit"), ("t2", "abort"), ("t3", "commit")]
    assert recover(log, "t1") == "commit"
    assert recover(log, "t2") == "abort"
    assert recover(log, "t3") == "commit"


def test_presumed_abort():
    """no record means no commit was ever promised"""
    assert recover([("t1", "commit")], "t9") == "abort"
    assert recover([], "t1") == "abort"
A hint

Only a participant that has not voted yes can still back out: once it is prepared, the coordinator may already have logged a commit. For recovery, look up the transaction's record in the log; presumed abort means a missing record is read as an abort.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Check yourself

Check yourself

3 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 3 In two-phase commit with a coordinator that presumes abort, what is the commit point?

    Choose one answer.

    Show the answer to question 1

    Answer: The coordinator writing commit to its own log

    Before the coordinator logs its decision, a crash still leads to abort, because no record means abort. Once commit is in its log, the transaction will commit on every participant, even if the coordinator crashes right after writing it.

  2. Question 2 of 3 A participant has voted yes, and the coordinator has been unreachable for a minute. What may the participant do with its prepared transaction?

    Choose one answer.

    Show the answer to question 2

    Answer: Keep waiting with its locks held, or ask another participant whether it knows the decision

    After a yes vote the decision is no longer the participant's: the coordinator may have logged commit or abort. Deciding alone could make it disagree with the others, so it waits or learns the outcome from someone who knows.

  3. Question 3 of 3 Every shard of a database is a Raft group of five servers. Why does a transfer between two shards still need two-phase commit?

    Choose one answer.

    Show the answer to question 3

    Answer: Each shard can refuse its part, and a commit needs every shard's yes, not a majority of servers

    Raft makes the five replicas of one shard agree on that shard's log. Whether a transfer commits depends on both shards accepting their part, for example a balance that must not go negative, so the shards still vote. Raft makes each vote and the decision durable and available.

Two-phase commit on PostgreSQL 18.6, with the blocking failure reproduced Pro: opens with a Pro, Premium or Ultimate pass

The simulator shows where the protocol waits; a real database shows what the wait costs. This part runs a transfer between two PostgreSQL servers whose coordinator stops right after its decision, and shows the locks that stay held through a restart.

In this part:

  • A transfer between two servers whose coordinator stops after deciding
  • What a prepared transaction holds while it waits, reproduced
  • Finishing in-doubt transactions safely: the recovery drill

1 runnable example (pglab.py, prepared_session.py) · about 2 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Consensus inside a shard: a Raft log simulated, and the rule that keeps commits Pro: opens with a Pro, Premium or Ultimate pass

Consensus keeps a shard’s replicas agreeing on one log, and one rule about votes is what keeps committed entries from disappearing. This part replays a partition step by step, then runs 500 random histories with and without that rule.

In this part:

  • Majorities, terms and the vote that checks the log
  • A partition replayed: an entry that was never committed disappears
  • 500 random runs, and the same runs without the up-to-date check
  • Two-phase commit over consensus groups, with the numbers of one commit

1 runnable example (Python) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

A saga with an outbox, run end to end, and how designs are judged Pro: opens with a Pro, Premium or Ultimate pass

A saga trades atomic commit for steps that can each fail on their own. This part runs one order saga three times, with the outbox and inbox in place and then with each of them removed, and ends with how a saga design is judged.

In this part:

  • Three runs: an outbox, a missing inbox, and a dual write
  • Compensation failures, and the step that decides
  • What a saga does not isolate, and the countermeasures
  • A rubric for a saga design

1 runnable example (Python) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

  1. Fresher to mid · Warm-up

    Why is two-phase commit called a blocking protocol?

    Because a participant can get stuck. Once it has voted yes, it has promised to commit if asked, so it may no longer abort on its own, and it cannot commit on its own either, since another participant may have voted no. If the coordinator crashes after collecting the votes and before the participant hears the decision, the participant must wait, holding its locks, until the coordinator recovers or until it learns the outcome from another participant that knows it. Every transaction that needs those rows waits behind it. Before voting yes a participant can still abort after a time-out, so the blocking window is exactly the time between its yes vote and its receipt of the decision. Replicating the coordinator's log with a consensus protocol, as some databases do, removes most of that window.

  2. Mid to senior

    After a deploy, an UPDATE on a busy PostgreSQL table hangs, and pg_prepared_xacts shows a transaction prepared 40 minutes ago by a payment coordinator that has since been replaced. What is going on, what do you do with the prepared transaction, and how do you prevent a repeat?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Senior to staff

    A teammate argues: "Every shard is a Raft group of three servers, so we can drop two-phase commit for transfers between shards." Is that right? Explain what each protocol guarantees and design a cross-shard transfer that does not block when a server fails.

    Model answer, Pro. First step and follow-ups

  4. Mid to senior

    A travel app books a train seat, a hotel room and a payment through three services with separate databases. Design the saga: the order of the steps, the compensations, how messages travel, and what happens when the hotel cannot be cancelled after the payment has been refunded.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Two-phase commit is atomic, and the coordinator’s logged decision is its commit point.
  • A participant that has voted yes waits for the decision with its locks held: that is where 2PC blocks.
  • Paxos and Raft make the replicas of one log agree by majority; shards still vote, so consensus complements 2PC.
  • A saga runs local transactions with compensations; an outbox and an inbox carry its messages, each applied once.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.