DlqStore is the SPI behind the @DeadLetterQueue annotation. It persists events that exhausted their retry budget so an operator (or an automated replay job) can investigate, fix, and re-process them. The MongoDB-backed implementation is auto-configured; any custom backend (Kafka topic, RabbitMQ queue, JDBC table, S3 bucket, …) plugs in by registering a DlqStore Spring bean.
When the SPI is exercised
Interface
Semantic contract
savemust store the entry underevent.id()and, if the backend supports it, set up native expiry frompolicy.retentionDays()(e.g. MongoDB TTL index, Kafka message TTL). WhenretentionDays == 0, entries are kept indefinitely.- The caller applies
policy.includeOriginalDocument()andpolicy.includeStackTrace()to theFailedEventpayload before callingsave. Implementations don’t re-check those flags — they just persist what they’re given. findByIdreturnsOptional.empty()for missing or already-expired entries.findByStreamNamereturns an empty list (nevernull) when no entry matches. Ordering is implementation-defined.
FailedEvent record
expiresAt is precomputed by the framework from createdAt + policy.retentionDays() (see DlqPolicy.computeExpiresAt). Backends with native TTL use this directly; backends without it can ignore the field and rely on a custom sweep.
DlqPolicy record
The backend-neutral view of @DeadLetterQueue:
@MongoDlqOptions(collection = "...")). Those live outside DlqPolicy — the implementation reads its own companion annotation directly from the @ChangeStream definition.
Provided implementations
The Redis backend (
flowwarden-redis) does not ship a DlqStore implementation — only LockService and CheckpointStore. DLQ persistence in a Redis-backed deployment falls back to the default MongoDlqStore (so MongoDB is still required), or to a user-provided custom bean.No-op default
DlqStore.noOp() returns a shared singleton that silently discards everything:
Providing a custom implementation
The auto-configured bean is guarded by@ConditionalOnMissingBean, so any @Bean DlqStore you declare wins. A custom implementation routes events by reading event.streamName() and resolving it to its backend resource (a Kafka topic, a Rabbit queue, a JDBC table, …):
MongoDlqStore’s pattern (per-stream resources resolved from a companion annotation), the framework exposes a registerStream(streamName, collection)-style hook at startup — see MongoDlqStore in the core module as the reference implementation, which uses this hook to create the TTL index.
Testing the contract
If you write a customDlqStore, run it against the contract tests published in flowwarden-stream-core-testkit:
DlqStoreContractTest and implement the abstract factory method. The contract covers save / findById / findByStreamName semantics and policy-driven expiry — the same suite MongoDlqStore is validated against.
See Also
@DeadLetterQueue annotation
The user-facing annotation that drives this SPI.
Retry & DLQ Guide
DLQ routing, manual sends, document schema, best practices.
CheckpointStore SPI
The sister SPI that backs
@Checkpoint.LockService SPI
The sister SPI that backs
DeploymentMode.SINGLE_LEADER.