Skip to main content

Overview

@Filter performs application-side filtering in Java. All events arrive from MongoDB, and a predicate decides which ones are forwarded to handler methods. Events that don’t pass the filter are silently skipped — a rejection is a terminal settlement, so their resume token still advances the lastProcessedToken checkpoint anchor. Unlike @Pipeline which runs a MongoDB aggregation pipeline once at stream startup to reduce network traffic, @Filter executes on every event and can leverage Spring beans, service calls, and any Java logic.
At most one @Filter method is allowed per @ChangeStream class.

Supported Signatures

Basic Example

Startup Validation (Fail-Fast)

FlowWarden validates @Filter compatibility at application startup and rejects clearly incompatible configurations immediately with a clear error message. The application will fail to start if @Filter is combined with a typed handler covering an operation where MongoDB does not provide a fullDocument.

Why?

@Filter predicates typically call ctx.getFullDocument(), which returns Optional.empty() for DELETE, DROP, and INVALIDATE operations. Running the filter on a typed handler dedicated to one of those operations is almost always a mistake, so FlowWarden rejects that combination at startup. Combining @Filter with the @OnChange catch-all is allowed — DROP and INVALIDATE will reach the predicate, and the predicate must handle Optional.empty() explicitly.

Rule

The following combination is rejected:
This configuration causes a BeanCreationException at startup — the application will not start.
@Filter + typed handler for a no-fullDocument operation
The error message:

Valid Combinations

The following combinations are accepted:

Quick Reference

If you need to handle DELETE events alongside other operations while filtering, use @Pipeline for server-side filtering, guard the @Filter predicate against Optional.empty(), or move the filter logic directly into your handler method.

Checkpoint Interaction

A rejection by @Filter is a terminal settlement: the event’s outcome is decided, so its token advances lastProcessedToken exactly like a handler success does (per the saveEveryN / saveIntervalSeconds policy). A filter-heavy stream therefore stays recoverable at resume cascade level 1 through its rejected traffic: rejections advance the anchor instead of stalling it. As with any settlement, a restart re-reads at most the events since the last confirmed write (up to N-1 with saveEveryN = N) — those rejections are simply re-evaluated by the filter. Rejected events never touch lastSeenToken, which holds exclusively server-certified positions written by the idle heartbeat (idleHeartbeatIntervalSeconds) — the safety net for periods where the stream settles nothing at all.

Combining with @Pipeline

@Filter can coexist with @Pipeline on the same @ChangeStream, forming a double filtering funnel: Typical use case: pre-filter operationType = insert | update and status = PAID server-side with @Pipeline, then verify application-side with @Filter that the tenant is active via a Spring service call.
When combining @Pipeline + @Filter, events that pass the server-side pipeline reach FlowWarden and settle terminally regardless of whether @Filter accepts them — accepted or rejected, they advance the lastProcessedToken anchor. When the pipeline filters out everything for a while, the idle heartbeat takes over and certifies fresh positions into lastSeenToken, preserving the resume cascade level-2 safety net.

See Also

@Pipeline

Server-side aggregation pipeline filtering

Filtering Events Guide

Complete guide combining @Pipeline and @Filter

@Checkpoint

Resume token persistence and dual checkpoint

Event Handlers

@OnInsert, @OnUpdate, @OnDelete, @OnChange