← Home ← Codex ← DEBT
Browse by Category
+ added · updated 7d
← Back to glossary

Transactional Outbox Pattern

Messaging Advanced
debt(d9/e7/b7/t9)
d9 Detectability Operational debt — how invisible misuse is to your safety net

Closest to 'silent in production until users hit it' (d9). The detection_hints field explicitly states automated detection is 'no', and the code_pattern hint (outbox|publish.*commit) is a regex grep, not a tool-based check. The dual-write gap — broker publish succeeding after DB commit fails, or vice versa — produces silent event loss that only surfaces when users notice missing events or data inconsistencies in production. No tool in the hints list catches this.

e7 Effort Remediation debt — work required to fix once spotted

Closest to 'cross-cutting refactor across the codebase' (e7). The quick_fix describes adding an outbox table to every service that publishes events, writing to it within the same transaction as domain data, and building an idempotent relay process. This is not a single-component fix — it requires schema changes per service, transaction scope changes, a new relay/poller infrastructure component, and cleanup jobs. The scope 'applies_to: web, cli, queue-worker' confirms the cross-cutting nature.

b7 Burden Structural debt — long-term weight of choosing wrong

Closest to 'strong gravitational pull' (e7). The outbox pattern applies across web, cli, and queue-worker contexts for every service that publishes events. Every new event-publishing feature must follow the outbox convention: write to outbox table in transaction, rely on relay. The relay process and outbox table schema become load-bearing infrastructure. Any service touching event publishing is shaped by this choice, though it doesn't redefine the entire system architecture.

t9 Trap Cognitive debt — how counter-intuitive correct behaviour is

Closest to 'catastrophic trap — the obvious way is always wrong' (t9). The misconception is stated directly: 'A DB transaction plus broker publish are atomic — they're not.' The obvious, intuitive approach (publish to broker inside or immediately after a DB transaction) is always wrong. The common_mistakes confirm this: dual-write without outbox is the default pattern developers reach for. The failure mode is silent event loss, making this exactly the kind of trap where the natural implementation is the broken one.

About DEBT scoring →

TL;DR

The Outbox pattern atomically saves domain events to an 'outbox' DB table in the same transaction as business data — a relay then publishes to the message broker, preventing lost events.

Explanation

Problem: update DB and publish message — if broker publish fails after DB commit, event is lost. Solution: write to an 'outbox' table in the same DB transaction as the domain data. A separate relay process reads the outbox and publishes to the broker, deleting rows after confirmed publish. This achieves atomic write-to-DB and guaranteed eventual publish. CDC (Change Data Capture): use Debezium to read DB transaction log and publish to Kafka — no outbox table needed. Polling relay: simpler, slight delay. Event store: Kafka as the source of truth (event sourcing). Implementations: Symfony Messenger Doctrine transport, Transactionally Outbox libraries.

Common Misconception

A DB transaction plus broker publish are atomic — they're not. The broker publish can fail after the DB commits, losing the event silently.

Why It Matters

Outbox pattern closes the dual-write consistency gap — the most common source of lost events in event-driven microservices.

Common Mistakes

  • Dual-write without outbox — publish to broker then DB commit (or vice versa) can leave them inconsistent.
  • Not cleaning up old outbox rows — table grows unboundedly.
  • Relay not idempotent — duplicate publish on relay restart.

Code Examples

✗ Vulnerable
// Dual-write — inconsistent on failure:
$db->beginTransaction();
$db->insert('orders', $orderData);
$db->commit();
$broker->publish('order.created', $event); // Fails? Event lost.
✓ Fixed
// Outbox pattern:
$db->beginTransaction();
$db->insert('orders', $orderData);
$db->insert('outbox', [
    'topic' => 'order.created',
    'payload' => json_encode($event),
    'created_at' => now(),
]);
$db->commit();
// Relay process reads outbox and publishes asynchronously
// Idempotent: broker publish + delete outbox row

Added 23 Mar 2026
Views 77
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings T 0 pings W 3 pings T 0 pings F 0 pings S 0 pings S 1 ping M 0 pings T 0 pings W 3 pings T 2 pings F 2 pings S 5 pings S 2 pings M 2 pings T 1 ping W 0 pings T 0 pings F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 1 ping F 1 ping S 1 ping S 1 ping M 0 pings T 0 pings W
No pings yet today
No pings yesterday
Scrapy 13 Amazonbot 10 Perplexity 9 SEMrush 5 Ahrefs 4 Unknown AI 3 Google 3 Bing 3 ChatGPT 2 Claude 2 PetalBot 2 Meta AI 1 Qwen 1 Sogou 1 Majestic 1
crawler 55 crawler_json 4 pre-tracking 1
DEV INTEL Tools & Severity
🟠 High ⚙ Fix effort: High
⚡ Quick Fix
Add outbox table to every service that publishes events. Write to outbox in same transaction as domain data. Build idempotent relay process. Delete rows after confirmed publish.
📦 Applies To
web cli queue-worker Symfony Messenger
🔗 Prerequisites
🔍 Detection Hints
outbox|publish.*commit|commit.*publish
Auto-detectable: ✗ No
⚠ Related Problems
🤖 AI Agent
Confidence: Low False Positives: High ✗ Manual fix Fix: High Context: File Tests: Update


✓ schema.org compliant