Reading transaction metadata
When the upstream writer commits inside a MongoDB transaction, every change event in that transaction carries the session id (lsid) and transaction number (txnNumber). FlowWarden surfaces them through ChangeStreamContext.getTransactionInfo() as an Optional<TransactionInfo>, empty for standalone writes.
No extra configuration is required — just the replica set already needed by Change Streams.
Mono<Void> and wraps the body in Mono.fromRunnable; the ctx.getTransactionInfo() call is identical.
Common use cases
- Audit / event grouping — bucket downstream log entries by
txnNumberso a single business operation surfaces as one logical record - Atomicity-preserving propagation — collect all events of one upstream transaction before publishing them downstream as a unit (Kafka batch, message envelope, projection rebuild)
- Correlation — propagate
txnNumberinto traces / spans so cross-service correlation matches the source-of-truth transaction boundary
flowwarden-examples repository — see imperative/10-transactions and reactive/10-transactions.
Prerequisites for exactly-once
MongoDB transactions require:- A replica set (already required for Change Streams)
- A
MongoTransactionManagerSpring bean in your application
Exactly-once pattern
Use Spring’s@Transactional on your handler method combined with ctx.saveCheckpointNow():
How it works
- Spring’s
@Transactionalopens a MongoDB session and binds it to the current thread - All
MongoTemplateoperations on the same thread automatically use the bound session ctx.saveCheckpointNow()saves the checkpoint throughMongoTemplate— it joins the same transaction- On success, Spring commits both the business write and the checkpoint atomically
- On failure, Spring rolls back everything — the event will be redelivered on the next attempt
ctx.saveCheckpointNow() is required inside the handler. Without it, FlowWarden saves the checkpoint after the handler returns — outside the transaction boundary.When to use transactions
Use transactions
- All handler writes go to MongoDB
- Duplicate processing has real consequences (double charges, wrong inventory)
- You need exactly-once guarantees
- You use
SINGLE_LEADERdeployment mode
Skip transactions
- Handler calls external APIs (HTTP, Kafka, email)
- Handler is naturally idempotent
- At-least-once is acceptable
- High-throughput streams where transaction overhead matters
Interaction with retry and DLQ
Transactions work naturally with@RetryPolicy and @DeadLetterQueue:
- Each retry attempt runs the handler in a new transaction
- If the handler throws, the transaction is rolled back (business writes + checkpoint)
- After all retries are exhausted, the event is sent to the DLQ outside any transaction
Important constraints
mongoTemplateRef breaks transaction atomicity
Spring binds the transaction session to a specific MongoDatabaseFactory. The checkpoint store uses the default MongoTemplate from auto-configuration. If your handler uses a different template via mongoTemplateRef, the checkpoint save and the handler’s writes use different database factories — they cannot participate in the same transaction.
saveEveryN must be 1
For transactional exactly-once, use @Checkpoint(saveEveryN = 1). With saveEveryN > 1, FlowWarden skips checkpoint saves between batches — events processed between checkpoints would not have transactional guarantees on crash recovery.
Handler must call saveCheckpointNow()
FlowWarden’s automatic checkpoint save happens after the handler returns — outside the @Transactional boundary. You must explicitly call ctx.saveCheckpointNow() inside the handler to include the checkpoint in the transaction. If you forget, FlowWarden falls back to at-least-once (the checkpoint saves outside the transaction).