Chaturmind
LearnDSASystem DesignDevOpsEngineering GrowthBlog
Start learning
Chaturmind

Structured learning paths for engineers who want to go deep. Written by practitioners.

Learn

  • Java
  • DSA
  • System Design
  • Spring Boot
  • AI / ML
  • DevOps
  • Engineering Growth

Company

  • Blog
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Chaturmind. All rights reserved.

Built for engineers who want to go deep.


← Spring Boot REST API Development

Spring Boot Basics

  • What is Spring Boot?
  • Dependency Injection
  • Building REST Controllers

Validation & Error Handling

  • Bean Validation with @Valid
  • Global Exception Handling

Spring Framework Internals

  • IoC Container Fundamentals
  • Bean Lifecycle In Detail
  • Component Scanning & Configuration
  • Auto-Configuration Mechanism
  • Spring AOP
  • @Transactional Deep Dive

Microservices Architecture

  • Monolith to Microservices Decomposition
  • API Gateway
  • Service Discovery
  • Inter-Service Communication Choices
  • Event-Driven Architecture Patterns
  • Messaging Technology Choices

Resilience Patterns

  • Configuration Management
  • Circuit Breaker Pattern
  • Retry & Backoff Strategies
  • Bulkhead & Rate Limiting
  • Timeout Strategy
  • Why Microservices Fail

Distributed Data & Consistency Patterns

  • Two-Phase Commit
  • Saga Pattern
  • Outbox Pattern
  • Eventual Consistency Design
  • CQRS Basics
  • CAP Theorem

Observability & Operations

  • Centralized Logging
  • Distributed Tracing
  • Metrics & Monitoring
  • Alerting Strategy
  • Health Checks

Payment Systems

  • Payment — Requirements
  • Payment — Core Flow
  • Payment — Idempotency Implementation
  • Payment — Failure Handling & Reconciliation
  • Payment — Security

Order Management System

  • OMS — Requirements
  • OMS — State Machine Design
Chaturmind
← Spring Boot REST API Development

Spring Boot Basics

  • What is Spring Boot?
  • Dependency Injection
  • Building REST Controllers

Validation & Error Handling

  • Bean Validation with @Valid
  • Global Exception Handling

Spring Framework Internals

  • IoC Container Fundamentals
  • Bean Lifecycle In Detail
  • Component Scanning & Configuration
  • Auto-Configuration Mechanism
  • Spring AOP
  • @Transactional Deep Dive

Microservices Architecture

  • Monolith to Microservices Decomposition
  • API Gateway
  • Service Discovery
  • Inter-Service Communication Choices
  • Event-Driven Architecture Patterns
  • Messaging Technology Choices

Resilience Patterns

  • Configuration Management
  • Circuit Breaker Pattern
  • Retry & Backoff Strategies
  • Bulkhead & Rate Limiting
  • Timeout Strategy
  • Why Microservices Fail

Distributed Data & Consistency Patterns

  • Two-Phase Commit
  • Saga Pattern
  • Outbox Pattern
  • Eventual Consistency Design
  • CQRS Basics
  • CAP Theorem

Observability & Operations

  • Centralized Logging
  • Distributed Tracing
  • Metrics & Monitoring
  • Alerting Strategy
  • Health Checks

Payment Systems

  • Payment — Requirements
  • Payment — Core Flow
  • Payment — Idempotency Implementation
  • Payment — Failure Handling & Reconciliation
  • Payment — Security

Order Management System

  • OMS — Requirements
  • OMS — State Machine Design
HomeLearnSpring BootSpring Boot REST API DevelopmentDistributed Data & Consistency Patterns
✓ FreeAdvanced· 7 min read

Outbox Pattern

The dual-write problem that breaks 'update the DB and publish an event' atomically, the outbox table fix, polling vs Change Data Capture, and the at-least-once guarantee it actually achieves.

Published September 23, 2026


Outbox Pattern

The dual-write problem

@Transactional
void createOrder(Order order) {
    orderRepository.save(order);           // write 1: to the database
    messagePublisher.publish(new OrderCreatedEvent(order)); // write 2: to the message broker
    // these are TWO SEPARATE SYSTEMS — there's no way to commit both atomically together
}

A database transaction and a message-broker publish are two entirely separate systems with no shared transaction coordinator (short of 2PC, which — per Two-Phase Commit — doesn't fit this context well) — there's a real gap where the database commit succeeds but the publish fails (broker temporarily unreachable), or the publish succeeds but the database transaction later rolls back for an unrelated reason. Either way, the database and the event stream disagree about what happened — this is the dual-write problem, and it's a genuine, common source of silent data/event inconsistency in event-driven systems.

The outbox table: making it one write, not two

@Transactional
void createOrder(Order order) {
    orderRepository.save(order);
    outboxRepository.save(new OutboxEvent("OrderCreated", serialize(order))); // SAME transaction, SAME database
}
CREATE TABLE outbox_events (
    id UUID PRIMARY KEY,
    event_type VARCHAR(100),
    payload JSONB,
    published BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMP
);

The fix: write the event into an outbox table, within the same database transaction as the business data — now there's only ever one atomic write (both the Order row and the OutboxEvent row commit or roll back together, since they're in the same transaction against the same database). The event is guaranteed to be durably recorded if and only if the business data change actually committed — no more dual-write gap, because there's no longer a second, separate system involved in the atomic part of the operation.

Polling publisher

@Scheduled(fixedDelay = 500)
void publishPendingEvents() {
    List<OutboxEvent> pending = outboxRepository.findByPublishedFalse();
    for (OutboxEvent event : pending) {
        messagePublisher.publish(event);
        event.setPublished(true);
        outboxRepository.save(event);
    }
}

A background job periodically reads unpublished outbox rows and actually sends them to the broker — the publish-to-broker step now happens outside the original transaction, asynchronously, decoupled from the request that created the order. The tradeoff: a small, bounded delay between the local commit and the event actually reaching the broker (proportional to the polling interval), and the polling itself adds a small continuous load on the outbox table.

Change Data Capture (CDC) alternative

Tools like Debezium take a different approach: instead of polling, they tail the database's own transaction log (the same write-ahead log ACID Properties covers as the durability mechanism) directly, streaming new outbox rows to the broker the moment they're committed — no polling delay, no extra load from repeated SELECT ... WHERE published = false queries. CDC is the more sophisticated, lower-latency option, at the cost of additional infrastructure (running and operating Debezium, or an equivalent) — polling remains the simpler, easier-to-reason-about starting point for many teams.

The guarantee actually achieved: at-least-once, never lost

Outbox delivers at-least-once delivery, not exactly-once — a crash between publishing to the broker and marking the outbox row as published could cause the same event to be published again on the next polling cycle (the row is still published = false), meaning a consumer might see the same event twice. What outbox does guarantee absolutely: the event is never silently lost — if the business data committed, the event is durably recorded and will eventually be published, possibly more than once but never zero times. This is exactly why Eventual Consistency Design's "idempotent consumers" guidance matters directly alongside outbox — the at-least-once guarantee shifts the duplicate-handling responsibility to the consumer side, which needs to be able to safely process the same event twice without a bad outcome.

Follow-up questions this topic invites — and their answers

Q: Why not just publish the event FIRST, then write to the database? A: Same fundamental problem in reverse — if the publish succeeds but the subsequent database write fails, consumers act on an event describing a business change that never actually happened, arguably worse than the original ordering, since a downstream system might take irreversible action based on an event that turns out to be false.

Q: How does the polling publisher avoid publishing the SAME outbox row twice under concurrent polling (e.g. two instances of the scheduled job)? A: The findByPublishedFalse() + save() sequence shown has the same check-then-act race as elsewhere in this course unless protected — a real implementation needs either a database-level SELECT ... FOR UPDATE SKIP LOCKED (letting concurrent pollers each grab different unclaimed rows) or a single active poller instance, otherwise two instances could both read the same unpublished row and both attempt to publish it.

Q: When would CDC's added infrastructure complexity be clearly worth it over polling? A: When publish latency genuinely matters (a user-facing feature needing near-real-time event propagation, not just eventual within a polling interval) or when outbox table write volume is high enough that repeated polling queries become a measurable load concern — for many internal, non-latency-critical event flows, polling's simplicity is the better tradeoff.

Q: Does the outbox table need its own cleanup/archival strategy? A: Yes — published rows accumulate indefinitely without an explicit cleanup job (deleting or archiving rows older than some retention window, once you're confident they've been successfully consumed), the same unbounded-growth concern as any other table without an eviction policy, covered generally in Denormalization & Schema Trade-offs' soft-delete/archival discussion.

Previous

Saga Pattern

Next

Eventual Consistency Design

AI Tutor

Lesson: Outbox Pattern

Quick actions

AI responses can be inaccurate. Verify critical information.