Distributed transactions are one of the harder problems in microservices architecture. Each service owns its data. There is no shared database, no two-phase commit across service boundaries. Yet business operations routinely span multiple services.

Here are the patterns I've found most useful.

The Saga pattern

Break the transaction into a sequence of local transactions, one per service. Each step publishes an event that triggers the next. If a step fails, compensating transactions undo the completed steps.

The key insight: eventual consistency is acceptable for most business operations. An order that takes a few seconds to fully confirm is fine. An order that leaves inventory, payment, and shipping permanently out of sync is not.

Two flavors: - Choreography — each service listens for events and reacts. Simple, but hard to follow the flow as the system grows. - Orchestration — a central saga orchestrator drives the sequence. More explicit, easier to reason about.

Compensating transactions

A compensating transaction undoes a completed local transaction. Not all operations are reversible — a sent email cannot be unsent — so you sometimes compensate by recording that the reversal happened rather than actually reversing it.

Design your services to support compensation from the start. It is much harder to add later.

Idempotent consumers

Network failures mean messages can be delivered more than once. Every consumer that handles a saga step needs to be idempotent: processing the same message twice should produce the same result as processing it once.

The usual approach: store a unique event ID alongside the operation. If the ID has been seen before, skip the operation and return the same result.

Outbox pattern

The hardest reliability problem in event-driven systems: how do you atomically update your database and publish an event?

The outbox pattern: write the event to an outbox table in the same local transaction as the business data change. A separate process reads from the outbox and publishes to the message broker. The event is only published after it has been committed to the database.

This eliminates the dual-write problem entirely.

What I would choose

For most systems: saga with orchestration, idempotent consumers, and the outbox pattern. This gives you auditability, debuggability, and reliability without needing distributed transactions.

Reserve choreography for simple cases where the event flow is stable and unlikely to change.