distributed-systems · intermediate

Transactional Outbox and Saga Patterns — Reliable Multi-Step Work

Start here

Two different problems show up as soon as you leave a single database transaction:

  1. Dual-write problem: you update the database and publish a message. One can succeed while the other fails → lost updates or phantom events.
  2. Multi-service workflow: placing an order touches inventory, payment, and shipping—each with its own database. You cannot wrap them in one simple ACID transaction across the network.
Two patterns help: You should care because “just publish after commit” still races, and “distributed 2-phase commit everywhere” is rarely what you want operationally.

What you will learn

  1. Explain the dual-write failure mode with a concrete timeline.
  2. Implement the outbox idea step by step.
  3. Define choreography vs orchestration sagas at a beginner level.
  4. Work a complete order workflow example.
  5. Know limitations (compensation is hard, not magic rollback).
  6. Avoid claiming “exactly-once” without evidence.

What you should know first

TopicWhy
ACID transactionsLocal transactions are the building block
Message queues / pub-subWhere outbox rows go
IdempotencyConsumers and compensations must be safe to retry

Words you need before we begin

TermPlain English
Dual writeUpdating two systems without one atomic commit covering both.
OutboxTable (or log) of messages to publish, stored with business data.
Relay / publisher processReads outbox and sends to the broker; marks published.
SagaWorkflow of local steps + compensations spanning services.
CompensationBusiness undo (refund, release stock)—not always perfect reverse.
OrchestrationA coordinator tells each step what to do.
ChoreographySteps react to each other’s events without a central boss.
At-least-onceMessages/steps may run more than once.

Simple story: registered mail ledger

You write in the office ledger “package 42 must go out” in the same notebook entry as “order 42 paid.” A clerk later walks the “to send” list to the post office. You never say “I paid” without a ledger line for mailing—and you never mail without a ledger.

Where the analogy stops: compensations (refunds) are messier than crossing out a line; partial failures need explicit design.

The problem: dual write without outbox

Timeline:

  1. Begin DB transaction; insert order ord_9.
  2. Commit order.
  3. Publish OrderCreated to Kafka → network fails.
  4. User sees success; search indexer never hears; warehouse silent.
Or reverse:
  1. Publish first.
  2. DB commit fails.
  3. Consumers process an order that does not exist.
Outbox makes **DB commit** the single atomic point for “business change + intent to notify.”

Step-by-step: transactional outbox

Step 1 — Add an outbox table

Columns idea: id, aggregate_type, aggregate_id, event_type, payload, created_at, published_at.

Step 2 — Same transaction

BEGIN
  INSERT order ...
  INSERT outbox(event_type='OrderCreated', payload=...)
COMMIT

Step 3 — Relay publishes

Background worker:

Use **idempotent publish** / dedupe keys so relay retries are safe.

Step 4 — Consumers remain idempotent

Outbox does not remove at-least-once consumer needs.

Step 5 — Variants

Step-by-step: saga idea

Step 1 — Break the business flow into local transactions

Example order:

  1. Create order (pending)
  2. Reserve inventory
  3. Charge payment
  4. Mark order confirmed / schedule shipment

Step 2 — Define success path and compensations

StepCompensation if later fails
Reserve inventoryRelease reservation
Charge paymentRefund (async, may lag)
Create orderCancel order

Step 3 — Choose coordination style

Orchestration is often easier to debug; choreography can couple event webs if messy.

Step 4 — Make every step idempotent and timeout-aware

Network retries will duplicate commands.

Step 5 — Accept compensation limits

You cannot un-send a real-world email perfectly; you send a correction. Money refunds take time. Sagas model business undo, not physics undo.

Visual mental model

sequenceDiagram
  participant API as Order API
  participant DB as Order DB
  participant OB as Outbox
  participant R as Relay
  participant K as Broker
  participant Inv as Inventory service

API->>DB: BEGIN order + outbox
DB-->>API: COMMIT
R->>OB: read unpublished
R->>K: publish OrderCreated
K->>Inv: deliver
Inv->>Inv: reserve stock local TX

Learning question: Where is atomicity guaranteed—across order+outbox or across order+inventory?

Caption: Outbox atomizes notification intent with local data; sagas coordinate cross-service steps without global ACID.

Complete worked example: place order

Starting situation

Services: Order, Inventory, Payment. Each has its own database. User clicks Buy.

Constraints

Decisions

ConcernPattern
Notify inventory/paymentOutbox from Order service after creating PENDING order
Multi-step confirmOrchestrated saga: reserve → charge → confirm
Fail after reserve before chargeCompensate: release stock; mark order cancelled
Fail after charge before confirmCompensate carefully: refund + release; alert if refund fails
Consumer duplicatesIdempotency keys per step

Execution (happy path)

  1. Order DB: insert order + outbox OrderPlaced.
  2. Relay publishes.
  3. Orchestrator (or choreography) reserves inventory.
  4. Charge with idempotency key ord_9.
  5. Mark confirmed; publish OrderConfirmed.

Failure sample

Payment declines: release inventory, mark order PAYMENT_FAILED, notify user. No silent half-state without status.

Outcome

Clear states and compensations. Limitations: refund lag, manual ops for rare dual failures, more moving parts than a modular monolith transaction.

How it works in production

Ownership

Order domain owns outbox for its events. Saga owner (orchestrator team) owns stuck-workflow alerts.

Failure modes

ModeRiskMitigation
Relay downGrowing outbox lagAlert on oldest unpublished age
Non-idempotent consumerDouble reserve/chargeKeys + unique constraints
Compensation failureMoney/stock driftRetry compensations + human queue
Chatty choreographyEvent spaghettiPrefer orchestration for complex flows
Long-lived locksPoor UXShort reservations with expiry
Claiming exactly-onceFalse safetyDocument at-least-once + idempotency

Trade-offs

ChoiceBenefitCost
OutboxReliable notify with local commitRelay ops, lag
Direct dual writeSimple codeLost/phantom messages
SagaCross-service workflowsComplex failure paths
Single DB monolith transactionSimple ACIDScaling/org coupling limits
2PC distributed transactionsStrong coupling of commitOperational fragility, rare fit

Compare with related concepts

ConceptDifference
Inbox patternDeduplicate incoming messages on the consumer side
CDCTurn DB changes into events; can implement outbox-like flows
Orchestration enginesTemporal/Cadence-style durable workflows (advanced)
2PC/XAGlobal commit protocol—different trade-offs

Common misunderstandings

  1. “Outbox gives exactly-once processing.”
It helps **not lose the publish intent**. Consumers can still see duplicates.
  1. “Compensation is automatic rollback.”
It is **business logic** you write and test.
  1. “Sagas need microservices.”
Even modular monoliths use saga-like steps for long workflows.
  1. “If publish is in the request thread after commit, we are fine.”
Process crash between commit and publish still loses the event without outbox/CDC.
  1. “One big distributed transaction is easier.”
Often harder to operate than explicit sagas.

Check your understanding

  1. What is a dual write?
  2. Why insert outbox rows in the same transaction as the order?
  3. What is a compensating action?
  4. Orchestration vs choreography in one sentence each?
  5. Does outbox remove the need for consumer idempotency?

Practice

  1. Draw a timeline where dual write loses an event.
  2. Design outbox columns for PaymentCaptured.
  3. Write saga steps + compensations for hotel booking (room + payment + email).
  4. List metrics for outbox lag and stuck sagas.
  5. Explain to a PM why “refund” is not always instant compensation.

Revision summary

Glossary

TermDefinition
Transactional outboxPersist messages with business data atomically, publish later.
SagaMulti-step workflow with compensations across local transactions.
CompensationBusiness-level undo of a previous step.
Dual writeTwo stores updated without shared atomicity.

Abbreviations and terminology

What to learn next

  1. Idempotency, retries, backoff
  2. Exactly-once vs practical deduplication
  3. CDC vs dual writes
  4. Distributed transactions / saga deep dive

Deeper production notes

Measuring outbox health

Alert on age of oldest unpublished outbox row, not only queue depth. A stuck relay with small depth still blocks business notifications. Track publish error rate and duplicate publish rate separately.

Saga timeouts and human intervention

Every saga step needs a timeout and a terminal state: succeeded, compensated, or needs_ops. Pure automatic compensation sometimes cannot finish (provider outage during refund). Design an operator console for rare stuck workflows instead of pretending 100% automation.

Testing strategy

Contract tests for events; integration tests that kill the process between commit and publish (outbox should still relay); saga tests that fail step 3 and assert compensations for steps 1–2. Untested compensations fail for the first time in production.

FAQ from first-time learners

Q: Can the outbox live in Redis?
A: If Redis is not in the same atomic commit as your business DB, you reintroduce dual write. Prefer the same transactional store as the business row.

Q: Is a saga slower?
A: It can add latency and complexity, but it matches real multi-system workflows.

Q: Who reviews this in production?
A: Treat money and inventory sagas as specialist-reviewed designs; this lesson is conceptual, not a compliance manual.

Track: Distributed Systems

Previous: Distributed Locks and Consensus — Coordinating Without a Single Boss Memory

Next: Exactly-Once Processing vs Practical Deduplication

Series: Kafka & Event Streaming

  1. Message Queues — Hand Work Off Without Blocking the User
  2. Kafka Architecture — Brokers, Partitions, ISR, and Consumers
  3. Kafka Partition Ordering and Delivery Guarantees
  4. Kafka Exactly-Once Semantics — What Is Actually Guaranteed
  5. Transactional Outbox and Saga Patterns — Reliable Multi-Step Work (this guide)
  6. Kafka Streams Introduction
  7. Kafka with Spring Boot Code Walkthrough

All series

By Shubham Jain

All articles · Study paths

Shubham Jain · Learning Lab