Once data lives in several databases, single-database ACID no longer covers a business operation.
When an operation spans several databases or services (charge a card, reserve stock, create a shipment), a local transaction can't make it atomic.
Two-phase commit (2PC) uses a coordinator. Phase 1: every participant prepares (makes the change durable but undecided) and votes yes or no. Phase 2: the coordinator tells all to commit, or all to abort. It gives atomicity, but participants hold locks while waiting, and if the coordinator fails after prepare they are stuck in doubt until it recovers. Databases support it (PREPARE TRANSACTION in PostgreSQL, XA in MySQL), but it is rarely used across microservices.
A saga splits the operation into local transactions, each with a compensating action that semantically undoes it (refund the charge, release the reservation). If step 3 fails, run compensations for steps 2 and 1. Sagas give eventual consistency, not isolation: other requests can see intermediate states, so design for that (e.g. a pending status).
A recurring sub-problem is dual writes: update the database and publish an event. If you do both directly, a crash between them loses one. The transactional outbox fixes this: write the event into an outbox table in the same local transaction as the data change; a separate relay (or change data capture from the log) publishes it to the broker. Consumers must be idempotent, because delivery is at-least-once.
Example
-- Transactional outbox: data change and event committed atomically
CREATE TABLE orders (id bigint PRIMARY KEY, status text NOT NULL);
CREATE TABLE outbox (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
topic text NOT NULL,
payload jsonb NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
sent_at timestamptz
);
BEGIN;
INSERT INTO orders VALUES (42, 'placed');
INSERT INTO outbox (topic, payload)
VALUES ('order.placed', '{"order_id": 42}');
COMMIT;
-- relay process: claim a batch, publish to the broker, mark as sent
SELECT id, topic, payload FROM outbox
WHERE sent_at IS NULL ORDER BY id LIMIT 100 FOR UPDATE SKIP LOCKED;Remember
- 2PC gives atomicity across nodes but blocks when the coordinator fails
- Sagas use local transactions plus compensating actions
- The outbox pattern avoids lost events from dual writes; consumers must be idempotent
Watch out for
- Writing to the database and then publishing to a broker in two independent steps.