Skip to content

Add transactional outbox with trace propagation - #7

Merged
dominik-kovacs merged 32 commits into
mainfrom
feature/transactional-outbox
Jul 27, 2026
Merged

Add transactional outbox with trace propagation#7
dominik-kovacs merged 32 commits into
mainfrom
feature/transactional-outbox

Conversation

@dominik-kovacs

Copy link
Copy Markdown
Contributor

Summary

Adds notify-spring-boot-outbox: a channel-agnostic transactional outbox so a notification can be persisted in the same database transaction as the business change that triggers it. Commit the transaction and delivery is guaranteed; roll it back and nothing is sent.

  • OutboxNotifier.enqueue(request) persists in the caller's transaction and returns the outbox entry id (deliberately not a provider message id — nothing has been sent yet).
  • OutboxRelay polls with FOR UPDATE SKIP LOCKED (safe across instances, no leader election) and delivers through the normal Notifier, so interceptors, events, and observations still apply.
  • Retries are durable and cross-poll: attempts/next_attempt_at live in the table, with capped exponential backoff and a terminal FAILED state. An undeserializable payload fails that entry immediately rather than blocking the batch.
  • Trace propagation — the W3C trace context is captured at enqueue and restored by the relay, so the delivery span descends from the request that enqueued it instead of starting an orphan trace. micrometer-tracing is optional; without it the outbox behaves exactly as before.
  • Auto-configures on a DataSource — adding the module is the opt-in, so there is no enabled flag. Supplies its own scheduler, so applications need not enable scheduling.
  • Storage sits behind an OutboxStore SPI (JdbcClient implementation ships); the canonical DDL is shipped but never auto-run against your database.

Docs: docs/outbox.md.

Test plan

  • 52 module tests: notifier, properties, relay (delivery/retry/backoff/poison pills/store-outage resilience), auto-configuration
  • JdbcOutboxStore against real PostgreSQL via Testcontainers, initialised from the shipped schema-notification-outbox.sql so the published DDL is itself covered
  • SKIP LOCKED concurrency test: two concurrent claimers receive disjoint rows
  • OutboxTracePropagationOtelTest drives the real enqueue → relay pipeline against an OpenTelemetry SDK and asserts the delivery span shares the request's trace id — verified to fail if the propagator is swapped for the no-op
  • examples/outbox-email: end-to-end proof against Postgres + Mailpit that the commit path delivers the email and the rollback path never sends
  • Full reactor verify green

Note: the outbox's Postgres tests require Docker, so CI's verify now needs it (~13s added).

… relay

Tests for the outbox module are deferred and will be written from scratch.
Matches the rest of the project (tools.jackson). Jackson 3 throws unchecked
JacksonException, so the rewrapping catch blocks are gone.
…rammatically

Drops the redundant spring.notify.outbox.enabled flag — adding the module is the
opt-in, matching how the channel modules activate. The relay now runs on the
application TaskScheduler at the interval from OutboxProperties, so the poll
interval has a single source of truth. Documents the feature.
Adds the configuration-processor so spring.notify.outbox.* keys autocomplete,
with additional metadata correcting the two int defaults the processor reads as
0. Drops the unused notify-spring-boot-autoconfigure dependency and declares
spring-context, spring-tx, and jackson-core instead of relying on transitives.
…greSQL

Creates examples/outbox-email — a Spring Boot app proving the transactional
outbox guarantee: notifications enqueued in a business transaction are delivered
if the transaction commits, never sent if it rolls back.

Demonstrates Order flow using JdbcClient, OutboxNotifier, and OutboxRelay.
Integration-tested with Testcontainers (PostgreSQL + Mailpit) for end-to-end
verification of commit/rollback semantics.
The relay required a TaskScheduler bean, which Boot only auto-configures when the
application opts into scheduling — so using the outbox meant annotating your own
application class. It now supplies a dedicated single-thread scheduler, keeping
relay polling off the application's shared scheduler too.

Also marks the new API @SInCE 1.1.0 and returns Optional from the example
repository instead of null.
…er in tests

poll() no longer lets a claimBatch failure escape to the scheduler: a database
outage is logged and the next interval retries, instead of dumping a stack trace
every cycle (and risking the scheduled task being cancelled).

Replaces the hand-written notifier stub with RecordingNotifier from notify-test.
…order

The no-op fallback relied on the nested tracing configuration being processed
first; a reorder would have silently disabled propagation with no test failing.
It is now conditional on the tracing beans being absent.
@dominik-kovacs
dominik-kovacs merged commit 4107dce into main Jul 27, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant