@Checkpoint, FlowWarden persists two anchors of different natures to MongoDB and reloads them on startup to continue from the last durable position. The guarantee is at-least-once: no event is lost as long as a usable anchor exists, and the tail that was not yet durable at crash time is re-delivered — which is why handlers should stay idempotent.
lastProcessedToken— the work anchor. Advances on every terminally settled event: handler success, rejection by@Filter, no matching handler, or a terminal skip/DLQ decision. Persisted by a count-or-time policy (saveEveryN/saveIntervalSeconds), whichever threshold is reached first.lastSeenToken— the position anchor. Holds exclusively server-certified positions written by the idle heartbeat (idleHeartbeatIntervalSeconds): when a stream settles nothing for a while, a bounded probe asks MongoDB to certify a fresh resume position, protecting an idle stream from oplog rollover for as long as probes keep succeeding within the oplog window.
lastProcessedToken first (strict at-least-once), the certified lastSeenToken as the safety net, then onHistoryLost.
Quick setup
Add@Checkpoint to any @ChangeStream class:
How it works
- Active stream — settlements anchor the position
- Idle stream — the heartbeat certifies a fresh position
Saving strategies
The processed anchor — count or time, whichever comes first
lastProcessedToken is persisted by a single policy with two thresholds. All writers (the count threshold, the timer, and the manual ctx.saveCheckpointNow()) are serialized per stream, and each write also refreshes lastHeartbeatTimestamp — a token just delivered from the oplog re-confirms the recoverable position.
saveEveryN — the count threshold. Persists the anchor at every N-th settled event. Settlements include handler successes, @Filter rejections, events with no matching handler, and terminal skip/DLQ decisions — an event still being retried never counts.
saveIntervalSeconds — the time threshold. Bounds how long a dirty anchor waits before being persisted. On an active-but-slow stream (where the count threshold rarely triggers), this caps the anchor’s age in wall-clock time. A clean anchor is never rewritten — a quiet tick writes nothing.
The certified position — idleHeartbeatIntervalSeconds
The sole writer of lastSeenToken. When the stream has settled nothing for this long, FlowWarden opens an ephemeral, bounded probe chained from the last settled position — same collection, same @Pipeline, same fullDocument options as the stream. If the server certifies the interval empty, the returned position is persisted: the resume point keeps tracking the oplog head with zero traffic — for as long as probes keep succeeding within the oplog window, so keep the interval well below that window.
A delivered event token is never written as
lastSeenToken. The position anchor accepts server-certified positions only. This is what makes resume safety structural: a restart that replays old events cannot regress the certified position, because replayed tokens are simply never candidates for it.A certified position never crosses unsettled work. If events are sitting undelivered (a lagging or stuck stream), the probe abstains and writes nothing — repeated abstentions surface as a
WARN so a stuck consumer is visible instead of being papered over.Three regimes, three thresholds
Start position
startPosition controls where the stream starts consuming when it boots.
Resume cascade
WhenstartPosition = RESUME, FlowWarden applies a fixed 3-level cascade on startup. Each level is tried in order — if the anchor is unusable (typically aged out of the oplog), it falls through to the next.
Why the certified position matters
Imagine a stream that emits one event per week on a busy cluster. ItslastProcessedToken points at last week’s event — long gone from a 24-hour oplog window. Without the heartbeat, every restart would escalate to onHistoryLost. With it, lastSeenToken was re-certified minutes ago: the cascade falls back to a near-head position, replaying nothing and losing nothing (there was nothing to deliver in between — that’s exactly what the certification proved).
Cascade vs startPosition = LATEST
The cascade only runs with startPosition = RESUME (the default). With LATEST, both persisted anchors are ignored and the stream always starts from “now”.
Attribute reference
The MongoDB-backed storage is auto-configured. For a Redis-backed deployment, drop in the
flowwarden-redis satellite — auto-configuration takes care of the rest. To plug in a different custom store (e.g. JDBC, in-memory for tests), provide your own CheckpointStore bean — see the CheckpointStore SPI reference.Internal storage
Checkpoints are stored in the_fw_checkpoints collection of your MongoDB database. The collection is created automatically on first use.
Each document is keyed by stream name and contains the two anchors and metadata:
lastHeartbeatTimestamp records the last time a recoverable position was confirmed — an anchor write or a successful certification. Its age is the operational signal for resume-point health: it never moves on probe abstentions or failures.
Checkpoints written by versions before 1.0.0-rc.4 may hold an event token in
lastSeenToken. They stay fully readable — the token remains a valid resume position — and the field adopts the certified-only semantics at the first successful certification.Common patterns
Exactly-once semantics
Exactly-once semantics
FlowWarden provides at-least-once delivery by default. A crash between processing and checkpointing causes the last few events to be reprocessed on restart.To achieve exactly-once, use Spring’s
@Transactional combined with ctx.saveCheckpointNow() to commit your business writes and the checkpoint atomically:ctx.saveCheckpointNow() is an advanced early acknowledgement: it persists the processed anchor while the handler is still running. Outside an atomic transaction, you take responsibility for having completed your effects before the call — if the handler fails afterwards, the event will not be re-delivered. Only a confirmed write counts: on a store failure the anchor stays dirty and the automatic policy retries it.See the Transactions guide for full setup instructions, constraints, and best practices.Disabling checkpoint for dev/test
Disabling checkpoint for dev/test
Omit
@Checkpoint entirely — no anchors are persisted and the stream always starts from the latest event:Resume cascade in action — a week of silence on a busy cluster
Resume cascade in action — a week of silence on a busy cluster
The scenario the idle heartbeat is designed for:
@Checkpoint(saveEveryN = 1)— the stream emits roughly one event per week, but the MongoDB cluster handles millions of unrelated writes per hour. The oplog window is about 24 hours.- The last event settled 6 days ago.
lastProcessedTokenpoints there — far outside the oplog window. - Every 5 minutes of idleness, the heartbeat probe certified a fresh position:
lastSeenTokenis minutes old. - The application restarts (rolling deploy, node failure — anything).
- The cascade tries
lastProcessedToken—ChangeStreamHistoryLost, aged out. - The cascade falls back to the certified
lastSeenTokenand resumes instantly near the oplog head, with aWARNlog and theonResumeFallbackToSeencallback (surfaced by the Reporter or your ownStreamMetricsProvider). Nothing was missed: every certification proved the interval behind it empty.
onHistoryLost = FAIL and the stream would have refused to start, requiring operator intervention.What happens when a handler gets stuck
What happens when a handler gets stuck
The heartbeat protects idle streams — deliberately not stuck ones. If a handler hangs while events keep arriving, those events sit undelivered, and the probe abstains rather than certifying past them: a certified position must never cross unsettled work, or a crash would silently skip the deliveries a resume owes you.What you observe instead:
lastProcessedToken and lastHeartbeatTimestamp stop moving, and after repeated abstentions FlowWarden logs a WARN (“matching events keep sitting undelivered”). That frozen heartbeat age is the operational signal — alert on it.Multiple streams, independent checkpoints
Multiple streams, independent checkpoints
Each stream has its own checkpoint entry in
_fw_checkpoints, keyed by stream name. Two streams on the same collection checkpoint independently:See Also
@Checkpoint reference
Full annotation reference
How it Works
Where checkpointing fits in the event processing pipeline
Filtering Events
How filtered events interact with resume tokens