ACID across services is hard. Three patterns cover the practical space: 2PC for short cross-shard transactions, Saga for long cross-service flows, TCC as the middle ground. Picking the right one for the use case avoids the 'distributed transactions are unsolvable' despair.
The fundamental problem
You want operations across services to either all succeed or all fail. Network is unreliable. Services crash. Some operations have side effects (charge card, send email) that can't be 'rolled back' in the database sense. There's no perfect solution; only patterns matched to constraints.
2PC — the blocking pattern
Coordinator asks all participants 'prepare'. If all yes, says 'commit'. Participants hold locks during the gap. Works for: short transactions, bounded participants, within one trust domain. Doesn't work: long transactions (lock contention), participant crash leaves coordinator blocked.
Saga — long-running compensable steps
Each step has an inverse (compensation). Step 1 succeeds, step 2 fails → run compensation for step 1. Works for: long flows (checkout, onboarding) where steps cross service boundaries. Caveat: weaker isolation; intermediate states visible to others.
TCC — Try, Confirm, Cancel
Each step: Try (reserve resources without committing), Confirm (commit if everyone Tried), Cancel (release reservations). Like 2PC but resource-level. Works for: inventory reservation, hotel-bookings-style workflows. Requires each service to model a 'reserved' state.
Outbox + idempotency
Whatever pattern, every step must be idempotent (because retries happen). And: write your state change + the outbound event in a single local transaction, publish via a separate worker. The outbox pattern bridges 'I committed locally' to 'the message reached the broker' atomically.