Skip to main content
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

  • save must store the entry under event.id() and, if the backend supports it, set up native expiry from policy.retentionDays() (e.g. MongoDB TTL index, Kafka message TTL). When retentionDays == 0, entries are kept indefinitely.
  • The caller applies policy.includeOriginalDocument() and policy.includeStackTrace() to the FailedEvent payload before calling save. Implementations don’t re-check those flags — they just persist what they’re given.
  • findById returns Optional.empty() for missing or already-expired entries.
  • findByStreamName returns an empty list (never null) 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:
The annotation also accepts backend-specific options (e.g. @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:
Suitable for tests that don’t care about DLQ persistence. Not a production choice — failed events that should land in a DLQ vanish silently.

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, …):
For a backend that mirrors 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 custom DlqStore, run it against the contract tests published in flowwarden-stream-core-testkit:
Extend 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.