You need to write to two databases atomically. Either both succeed or both roll back. No partial state.

Two-phase commit (2PC) solves this. It’s also the reason distributed transactions have a bad reputation.

The Protocol#

A coordinator manages the transaction across participants (databases, services).

Phase 1: Prepare. Coordinator asks each participant: “Can you commit?” Each participant writes to its WAL, acquires locks, and votes YES or NO.

Phase 2: Commit/Abort. If all voted YES, coordinator sends COMMIT. If any voted NO, coordinator sends ABORT. Participants act accordingly.

%%{init: {'theme':'base', 'themeVariables': { 'primaryColor':'#000000','primaryTextColor':'#00ff00','primaryBorderColor':'#00ff00','lineColor':'#00ff00','secondaryColor':'#000000','tertiaryColor':'#000000','noteBkgColor':'#000000','noteBorderColor':'#00ff00','noteTextColor':'#00ff00'}}}%% sequenceDiagram autonumber participant C as Coordinator participant A as DB A participant B as DB B C->>A: PREPARE C->>B: PREPARE A-->>C: YES (locks held) B-->>C: YES (locks held) C->>A: COMMIT C->>B: COMMIT A-->>C: ACK B-->>C: ACK

The Blocking Problem#

Between PREPARE and COMMIT, participants hold locks and wait. If the coordinator crashes during this window, participants are stuck. They can’t commit (didn’t get the order) and can’t abort (already voted YES). Those locks stay held until the coordinator recovers.

This is why 2PC is called a blocking protocol. And it’s why it falls apart across service boundaries where you can’t guarantee coordinator availability.

2PC vs Saga#

The saga pattern replaced 2PC for most microservice architectures. Instead of locking everything and coordinating, sagas execute steps sequentially with compensating actions on failure. No global locks, no coordinator bottleneck. The trade-off: sagas give you eventual consistency instead of strict atomicity.

2PC still lives inside databases though. MySQL XA transactions use 2PC when a single query touches multiple storage engines. Raft consensus itself is a more sophisticated version of the same idea: get everyone to agree before committing.

At Oracle, we initially used XA transactions to keep the NSSF registration table and the config database in sync. It worked, but the prepare phase added 15-20ms of latency per transaction because both databases held locks waiting for the coordinator’s decision. We eventually moved to a transactional outbox pattern, writing to one database and propagating via CDC. Faster, but now eventually consistent instead of immediately consistent.

What I’m Learning#

2PC is elegant in theory and painful in practice. The fundamental issue is availability: you need every participant and the coordinator to be up for the transaction to complete. In a world of microservices and network partitions, that’s a strong assumption. But understanding 2PC matters because the problems it solved (and the problems it created) shaped every distributed transaction pattern that came after it.

When do you reach for distributed transactions vs eventual consistency?