CheckpointStore is the SPI behind the @Checkpoint annotation. It persists the two checkpoint anchors (lastProcessedToken — the last terminally settled event, lastSeenToken — the last server-certified position) that drive at-least-once delivery and stream resume on restart. The MongoDB-backed implementation is auto-configured; the Redis backend ships an alternative; any custom backend (JDBC, Cassandra, in-memory, …) plugs in by registering a CheckpointStore Spring bean.
When the SPI is exercised
The dual-anchor contract (why the two anchors have different natures and writers) is documented in the @Checkpoint reference.
Interface
save, findByStreamName and delete are abstract — every other operation has a working default built on them, so a minimal backend is three methods.
Semantic contract
savemust upsert the record keyed bycheckpoint.streamName().- Partial operations must preserve the fields they do not target, atomically with respect to concurrent writers. The two anchors have independent writers on different threads: the processed-anchor policy writes
lastProcessedTokenwhile the heartbeat probe writeslastSeenToken. A targeted update (partial$set,HSET,UPDATE … SET <pair>) satisfies this by construction. Thedefaultimplementations are read-modify-write fallbacks kept for source compatibility: under concurrent anchor writers they can resurrect a stale value of the other anchor, and a Java-level lock cannot fix this for a store shared between instances. Production implementations must override them with targeted writes — the shipped Mongo and Redis stores do. - The 4-argument overloads exist so the anchor and its
lastHeartbeatTimestampconfirmation land in one atomic write. Their defaults perform two writes (anchor, then heartbeat) — correct but observable in an intermediate state. resetAfterHistoryLostcarries the at-least-once guard of the recovery: the expired processed pair is removed only whilelastProcessedTokenstill equals the value read at history-loss detection. If a fresh anchor was re-acquired concurrently, it must be preserved. The race-free guarantee requires evaluating the guard atomically with the removal (a conditional write) — the Mongo stores put it in the update filter, the Redis stores use a Lua script.findByStreamNamemust returnOptional.empty()when no record exists for that stream.deleteis a no-op when no record exists.
Checkpoint record
lastSeenToken populated but no lastProcessedToken. The framework’s resume cascade tolerates any nullability pattern.
Provided implementations
For a Redis-backed deployment, the
flowwarden-redis satellite ships RedisCheckpointStore (blocking, auto-configured) and ReactiveRedisCheckpointStore (reactive, opt-in). Both override the partial operations with targeted HSET operations and resetAfterHistoryLost with a Lua script. See Redis Configuration for the storage layout.
No-op default
CheckpointStore.noOp() returns a shared singleton that silently ignores all calls:
Providing a custom implementation
The auto-configured bean is guarded by@ConditionalOnMissingBean, so any @Bean CheckpointStore you declare wins:
Overriding the partial operations
For any production backend, override every partial operation with a targeted write that touches only the relevant fields. Two traps the naive version falls into:- Every partial operation must upsert. On a brand-new stream, the bootstrap’s
saveSeenis the first write — a plainUPDATEmatches zero rows, silently persists nothing, and the core then runs believing its position is durable. UseINSERT … ON CONFLICT DO UPDATE(orMERGE). resetAfterHistoryLostmust evaluate its guard atomically with the removal — a conditional write, not a read-then-write.
Testing the contract
If you write a customCheckpointStore, run it against the contract tests published in flowwarden-stream-core-testkit:
CheckpointStoreContractTest and implement the abstract factory method. The contract covers upsert / find / delete semantics, the field-isolation invariant of every partial operation (a seen write never touches the processed pair and vice versa), the anchor+heartbeat overloads, and the resetAfterHistoryLost guard semantics — the same suite the built-in Mongo and Redis stores are validated against.
See Also
@Checkpoint annotation
The user-facing annotation that drives this SPI.
Redis backend
Drop-in Redis implementations of
CheckpointStore and LockService.LockService SPI
The sister SPI that backs
DeploymentMode.SINGLE_LEADER.DlqStore SPI
Failed-event persistence for the
@DeadLetterQueue annotation.