Design a Transactional Outbox & Event-Driven Ledger
1. Problem Statement & Scope Clarification
System Mission
Design a mission-critical, enterprise-grade Transactional Outbox & Event-Driven Ledger Platform engineered to guarantee atomic database state transitions and reliable cross-boundary asynchronous event propagation without distributed transactions (Two-Phase Commit / 2PC).
The platform must eliminate the notorious Dual-Write Problem, guarantee zero lost domain events, enforce double-spending prevention under high-concurrency balance updates, and scale outbox dispatching to tens of millions of daily events while avoiding catastrophic PostgreSQL MVCC table bloat and WAL (Write-Ahead Log) replication-slot disk exhaustion.
text+---------------------------------------------------------------------------------------------------------+ | TRANSACTIONAL OUTBOX & EVENT LEDGER AT A GLANCE | +------------------------------------+------------------------------------+-------------------------------+ | Scale: 50M Events/Day | Ingestion Peak: 5,000 TPS | Event Latency: < 5ms (Reactive Wake-Up) | | Read QPS: 25,000 Peak | P99 Lock Wait: < 2ms (In-Memory) | Durability: 100% Zero Loss | | Concurrency: Advisory Lock (SHM) | Event Bus: Redpanda / Amazon MSK | Primary DB: Aurora PostgreSQL | +------------------------------------+------------------------------------+-------------------------------+
Functional Requirements
- Atomic Dual-Write Prevention (Transactional Outbox): Ingest state mutations (financial transfers, coin purchases, order status updates) and atomically persist domain events within the same local relational database transaction.
- Guaranteed At-Least-Once Delivery: Reliably extract, dispatch, and stream outbox events to a distributed message log (Redpanda / Amazon MSK / Kafka) even across process restarts, application crashes, and network partitions.
- Ironclad Account Concurrency & Double-Spend Defense: Serialize balance checks, debits, and credits for a given account to prevent race conditions and overdrafts, ensuring zero lost updates.
- Idempotent Downstream Consumption: Enable downstream consumers (billing, analytics, notification, audit ledgers) to deduplicate re-delivered messages using monotonic idempotency keys without duplicate side effects.
- Pluggable Event Pipeline with Graceful Degradation: Maintain high availability on single-node deployments and developer environments (via in-process channel buses) while seamlessly scaling to partitioned distributed event brokers under high load.
Non-Functional Requirements (SLAs & SLOs)
- Data Consistency & Durability: zero data loss. Monetary mutations and outbox records must be bound by strict ACID guarantees (
SERIALIZABLEorREAD COMMITTEDwith explicit locking). - Availability: uptime for transactional ingress. Background event dispatch must degrade gracefully without taking down API write availability if the external message broker is temporarily unreachable.
- Latency (P99):
- Synchronous Transaction Commit: .
- Concurrency Lock Acquisition: .
- Outbox-to-Broker Dispatch Latency: (Reactive In-Memory Signal Sweeper) with fallback safety timeout, or (Log-Based CDC).
- Storage Stability: Absolute immunity to unbounded storage bloat. The system must prevent PostgreSQL table bloat (dead tuples) and replication-slot WAL accumulation from exhausting disk capacity.
2. Capacity & Scale Estimation (Back-of-the-Envelope Math)
Transaction Volume & Event Throughput (TPS)
- Daily Domain Events: ().
- Average Event Generation TPS:
- Peak Flash-Load TPS ( burst during market open / peak hours):
- Read & Account Balance Inquiries ( Read/Write Ratio):
Outbox Storage & WAL Generation Math
Each outbox record consists of:
event_id(UUID):event_type(VARCHAR):aggregate_id(VARCHAR):occurred_at(TIMESTAMPTZ):payload(JSONB):source_module(VARCHAR):- B-Tree Index & Row Header Overhead:
- Total Record Size: .
Storage Impact Under Different Outbox Architectures:
- Naive Retain-All Outbox (Marking
status = 'PROCESSED'): In addition, PostgreSQL MVCC creates a dead tuple for every update, doubling disk write amplification. - Immediate Delete-on-Success Architecture:
- Under Naive 2-Second Polling:
- Under Reactive In-Memory Signal Wake-Up ( dispatch): Events are drained immediately upon transaction commit. The active pending row queue depth stays bounded by the single batch size (), maintaining an active footprint well under during normal operations and bounded to during external broker retries. Disk usage remains permanently locked under 15 MB!
PostgreSQL Shared Memory Advisory Lock Sizing
PostgreSQL maintains advisory locks in shared memory (SHM), allocated at server startup:
- Each lock entry occupies approximately of shared memory.
- Total Shared Memory Footprint: .
- Disk I/O and WAL Overhead for Advisory Locks: Exactly 0 bytes/sec.
3. AWS-First High-Level Architecture
The architecture coordinates three specialized planes: Transactional Ingress & Ledger Persistence, Outbox Event Sweeping & Streaming, and Idempotent Consumer Materialization.
Synthesizing vector architecture diagram...
End-to-End Execution Walkthrough
| Step # | Stage / Component | Action Description | Concurrency / Storage Coordinate | State Transition |
|---|---|---|---|---|
| Step 1 | Ingress API | Client submits transfer request with Idempotency-Key header | HTTP POST /v1/transfers | Payload validated; rate limiter evaluated |
| Step 2 | Concurrency Lock | API begins transaction and acquires advisory lock | SELECT pg_advisory_xact_lock(hash(account_id)) | In-memory lock held in Postgres SHM; parallel transfers queue |
| Step 3 | Ledger Mutation | Debit source, credit destination in coin_ledger_entries | Relational table write in Aurora PostgreSQL | Balance verified ; row appended |
| Step 4 | Outbox Staging | Atomic INSERT INTO outbox_messages in the same transaction | Same PostgreSQL transaction context | Message staged with EventEnvelope format |
| Step 5 | Transaction Commit | COMMIT executes; advisory lock automatically releases | PostgreSQL Write-Ahead Log flushed to Aurora storage | Both ledger and outbox row are durably persisted |
| Step 6 | Reactive Wake-Up | Domain service triggers in-memory signal; worker wakes up instantly () | In-memory channel unblocks WaitForSignal | Worker queries batch of 50 pending records immediately |
| Step 7 | Event Dispatch | Outbox worker publishes event to Redpanda / Amazon MSK | Kafka producer writes to hitechies.economy.events | Message committed with monotonic broker offset |
| Step 8 | Delete-on-Success | Outbox worker immediately deletes published rows from DB | DELETE FROM outbox_messages WHERE id = @id | Zero dead tuples; table size remains |
| Step 9 | Downstream Consume | EconomyEventConsumer receives event envelope from broker | Consumer group offset tracking | Payload matched against EventTypeRegistry |
| Step 10 | Idempotency Apply | Consumer executes balance update with unique SourceEventId | INSERT ... ON CONFLICT (source_event_id) DO NOTHING | Exactly-once business effect achieved |
4. API Interface Design & Data Contracts
1. Execute Ledger Transfer (POST /v1/ledger/transfers)
Executes a balance transfer between accounts with atomic outbox event staging.
Request Headers
httpPOST /v1/ledger/transfers HTTP/1.1 Host: api.hispeeddesign.com Authorization: Bearer <jwt_token> Idempotency-Key: 7b8c2f1e-9a4d-4e6f-8b2c-1d3e5f7a9b1c X-Correlation-ID: corr_01J8N6K3P4V9QZ2W8M1Y7R4X Content-Type: application/json
Request Payload
json{ "source_account_id": "00000000-0000-0000-0000-000000000001", "destination_account_id": "99999999-9999-9999-9999-999999999999", "amount": 250, "currency": "COINS", "reason": "SUBVARIANT_UNLOCK_FEE", "reference_id": "pv_binary_search_monotonic" }
Response: 201 Created
json{ "transfer_id": "xfer_882910492817", "source_account_id": "00000000-0000-0000-0000-000000000001", "amount": 250, "new_balance": 1750, "status": "COMMITTED", "outbox_event_id": "evt_44a2c890-3b1a-4c2d-9f0e-8a7b6c5d4e3f", "timestamp": "2026-09-19T22:30:00.000Z" }
2. Standardized Outbox Event Envelope Specification (EventEnvelope)
All outbox events published to Redpanda / Amazon MSK adhere to a strict, decoupled JSON schema format that avoids CLR/JVM reflection dependencies:
json{ "eventId": "44a2c890-3b1a-4c2d-9f0e-8a7b6c5d4e3f", "eventType": "economy.coins.transferred.v1", "version": 1, "occurredAt": "2026-09-19T22:30:00.124Z", "sourceModule": "Economy", "correlationId": "corr_01J8N6K3P4V9QZ2W8M1Y7R4X", "payload": { "transferId": "xfer_882910492817", "sourceAccountId": "00000000-0000-0000-0000-000000000001", "destinationAccountId": "99999999-9999-9999-9999-999999999999", "amount": 250, "reason": "SUBVARIANT_UNLOCK_FEE", "referenceId": "pv_binary_search_monotonic" } }
5. Data Models & Storage Architecture
PostgreSQL DDL Schema
sql-- 1. Immutable Financial Ledger Schema (Double-Entry Ledger) CREATE TABLE economy.coin_ledger_entries ( entry_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), student_account_id UUID NOT NULL, amount BIGINT NOT NULL, -- Positive = Credit, Negative = Debit entry_type VARCHAR(32) NOT NULL, -- 'EARN', 'SPEND', 'TRANSFER', 'BONUS' reason VARCHAR(128) NOT NULL, source_event_id UUID UNIQUE, -- Idempotency anchor for incoming events created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_ledger_student_created ON economy.coin_ledger_entries (student_account_id, created_at DESC); -- 2. Fast Balance Snapshot Table (O(1) Balance Reads) CREATE TABLE economy.student_balances ( student_account_id UUID PRIMARY KEY, current_balance BIGINT NOT NULL CHECK (current_balance >= 0), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- 3. High-Performance Transactional Outbox Table CREATE TABLE economy.outbox_messages ( id UUID PRIMARY KEY, event_type VARCHAR(128) NOT NULL, version INT NOT NULL DEFAULT 1, occurred_at TIMESTAMPTZ NOT NULL, payload JSONB NOT NULL, source_module VARCHAR(64) NOT NULL ); -- B-Tree Index optimized for FIFO sweeping CREATE INDEX idx_outbox_occurred_at ON economy.outbox_messages (occurred_at ASC); -- 4. Downstream Idempotent Consumer Deduplication Store CREATE TABLE economy.processed_events ( event_id UUID PRIMARY KEY, event_type VARCHAR(128) NOT NULL, consumer_group VARCHAR(64) NOT NULL, processed_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Auto-cleanup partition or index for 30-day deduplication window CREATE INDEX idx_processed_events_time ON economy.processed_events (processed_at) WHERE processed_at < NOW() - INTERVAL '30 days';
Unlock Complete Architecture & Production Runbooks
You have explored the free architectural preview (~32%). Spend 1 Coin to unlock the remaining 6 production deep-dive sections for a full 24 hours.