Skip to main content
FlowWarden dispatches MongoDB Change Stream events to handler methods annotated with @OnChange, @OnInsert, @OnUpdate, @OnDelete, or @OnReplace. These annotations go on methods inside a @ChangeStream class.

Overview

There are two types of handler annotations:
TypeAnnotationsPurpose
Generic@OnChangeCatches all (or filtered) operation types
Typed@OnInsert, @OnUpdate, @OnDelete, @OnReplaceRoutes events by specific operation type

Dispatch Priority

When an event arrives, FlowWarden resolves the handler in this order:
  1. Typed handler matching the operation type (@OnInsert for INSERT, @OnUpdate for UPDATE, etc.)
  2. @OnChange fallback — only if no typed handler matches, and the event passes the operationTypes filter
Typed handlers always take priority. @OnChange is only invoked for operation types that have no dedicated typed handler in the class.

@OnChange

Generic handler for all Change Stream event types. Acts as a catch-all when no typed handler matches.
@ChangeStream(documentType = Order.class)
public class OrderHandler {

    @OnChange
    void handle(ChangeStreamContext<Order> ctx) {
        System.out.println(ctx.getOperationType() + " on " + ctx.getCollectionName());
    }
}
Rules:
  • Only one @OnChange method is allowed per @ChangeStream class.
  • Signature must use the CONTEXT_ONLY style — void handle(ChangeStreamContext ctx) or Mono<Void> handle(ChangeStreamContext ctx).

Filtering by Operation Type

Use the operationTypes attribute to restrict which events @OnChange handles:
@OnChange(operationTypes = { OperationType.INSERT, OperationType.UPDATE })
void handleWriteOps(ChangeStreamContext<Order> ctx) {
    // Only called for INSERT and UPDATE — not DELETE or REPLACE
}
When operationTypes is empty (the default), all operation types are accepted.

Typed Handlers

@OnInsert

Called when a new document is inserted into the collection.
@OnInsert
void onInsert(Order doc, ChangeStreamContext<Order> ctx) {
    log.info("New order: {}", doc.getId());
}

@OnUpdate

Called when an existing document is updated.
@OnUpdate
void onUpdate(ChangeStreamContext<Order> ctx) {
    log.info("Order updated: {}", ctx.getDocumentKey());
}
For UPDATE events, fullDocument may be null by default — MongoDB only sends the change delta. To get the full document on updates, enable UPDATE_LOOKUP on the MongoDB Change Stream options.

@OnDelete

Called when a document is deleted.
@OnDelete
void onDelete(ChangeStreamContext<Order> ctx) {
    log.info("Order deleted: {}", ctx.getDocumentKey());
}
For DELETE events, the full document is always null. If you use a document-typed signature like void onDelete(Order doc, ...), the doc parameter will be null. Prefer the CONTEXT_ONLY signature and use ctx.getDocumentKey() to identify the deleted document.

@OnReplace

Called when a document is replaced entirely (e.g. via MongoTemplate.save() on an existing document).
@OnReplace
void onReplace(Order doc, ChangeStreamContext<Order> ctx) {
    log.info("Order replaced: {}", doc.getId());
}

Rules for Typed Handlers

  • At most one method per typed annotation per class (e.g. you cannot have two @OnInsert methods)
  • If no typed handler matches the event, @OnChange is used as fallback (if present)
  • If neither a typed handler nor @OnChange matches, the event is silently skipped

Combining Annotations on the Same Method

You can place multiple typed annotations on a single method to handle several operation types with the same logic:
@OnInsert
@OnUpdate
void handleWriteOps(ChangeStreamContext<Order> ctx) {
    // Called for both INSERT and UPDATE events
    log.info("Write operation: {}", ctx.getOperationType());
}
This is equivalent to @OnChange(operationTypes = {INSERT, UPDATE}), but uses the typed handler dispatch path (which takes priority over @OnChange).
This is especially convenient when used with @Filter, since @Filter requires that all covered operation types have a fullDocument. Combining @OnInsert and @OnUpdate is safe — both have a full document available.
Combining annotations with and without fullDocument on the same method (e.g. @OnUpdate @OnDelete) emits a warning at startup. DELETE, DROP, and INVALIDATE events have no full document — the document parameter will be null for those events. Either use separate methods or handle the null case explicitly in your handler.
These two approaches produce the same behavior:
// Approach 1: Multiple typed annotations
@OnInsert
@OnUpdate
void handle(ChangeStreamContext<Order> ctx) { ... }

// Approach 2: @OnChange with operationTypes filter
@OnChange(operationTypes = { OperationType.INSERT, OperationType.UPDATE })
void handle(ChangeStreamContext<Order> ctx) { ... }
The key difference: typed handlers have higher dispatch priority than @OnChange. If you also have a separate @OnChange fallback in the same class, the typed annotations will be resolved first.

Supported Signatures

Parameter Styles

Typed handler methods (@OnInsert, @OnUpdate, @OnDelete, @OnReplace) support three parameter styles:
StyleSignatureDescription
CONTEXT_ONLYhandle(ChangeStreamContext<T> ctx)Access context; get the document via ctx.getFullDocument().
DOCUMENT_ONLYhandle(T doc)Receive the deserialized document directly.
DOCUMENT_AND_CONTEXThandle(T doc, ChangeStreamContext<T> ctx)Both the document and the context.
@OnChange only supports CONTEXT_ONLY.
DOCUMENT_ONLY and DOCUMENT_AND_CONTEXT signatures require a concrete documentType on @ChangeStream (not Document.class). Otherwise, startup fails with a validation error.

Return Types — Mode Exclusivity

Handler methods must use the return type that matches the configured flowwarden.default-mode:
ModeRequired Return TypeDescription
IMPERATIVEvoidBlocking handler. Mono<Void> is rejected at startup.
REACTIVEMono<Void>Non-blocking handler. void is rejected at startup.
Mixing return types is not allowed. All handler methods in an application must consistently use either void (imperative) or Mono<Void> (reactive), matching the flowwarden.default-mode. A mismatch causes a BeanCreationException at startup.
This gives the following full signature matrix:
StyleSignature
CONTEXT_ONLYvoid handle(ChangeStreamContext<T> ctx)
DOCUMENT_ONLYvoid handle(T doc)
DOCUMENT_AND_CONTEXTvoid handle(T doc, ChangeStreamContext<T> ctx)
Any other return type (e.g. CompletableFuture, Flux, String) is rejected at startup.

Examples

Typed Handlers with @OnChange Fallback

This example handles INSERT, UPDATE, and DELETE with typed handlers, and uses @OnChange as a fallback for REPLACE events.
@ChangeStream(name = "typed-order-capture", documentType = Order.class)
public class TypedOrderHandler {

    @OnInsert
    void onInsert(Order doc, ChangeStreamContext<Order> ctx) {
        log.info("New order: {}", doc.getId());
    }

    @OnUpdate
    void onUpdate(ChangeStreamContext<Order> ctx) {
        log.info("Order updated: {}", ctx.getDocumentKey());
    }

    @OnDelete
    void onDelete(ChangeStreamContext<Order> ctx) {
        log.info("Order deleted: {}", ctx.getDocumentKey());
    }

    @OnChange
    void onFallback(ChangeStreamContext<Order> ctx) {
        // Called for REPLACE and any other unhandled operation type
        log.info("Fallback: {} on {}", ctx.getOperationType(), ctx.getDocumentKey());
    }
}

Combined Annotations with @Filter

From the sample-spring-mvc module — a handler that reacts to both INSERT and UPDATE with a client-side filter:
@ChangeStream(documentType = Order.class)
@Checkpoint
public class OrderStreamWithFilter {

    @OnInsert
    @OnUpdate
    void handleOrderChange(ChangeStreamContext<Order> ctx) {
        System.out.println(ctx.summary());
    }

    @Filter
    boolean filterOrder(ChangeStreamContext<Order> ctx) {
        Optional<Order> doc = ctx.getFullDocument(Order.class);
        return doc.isPresent() && doc.get().getStatus().equals("CONFIRMED");
    }
}
This works because both INSERT and UPDATE events have a fullDocument available, which is required by @Filter. Combining @OnInsert @OnDelete with @Filter would fail at startup because DELETE events have no full document.

Minimal — Single @OnChange

The simplest form: one handler for all event types.
@ChangeStream(documentType = Order.class)
public class OrderHandler {

    @OnChange
    void handle(ChangeStreamContext<Order> ctx) {
        System.out.println(ctx.summary());
    }
}

Event Capture for Testing

From the flowwarden-samples project — a reusable handler that captures events for test assertions:
@ChangeStream(documentType = Order.class)
public class OrderEventCapture {

    private final List<CapturedEvent> events = new CopyOnWriteArrayList<>();

    @OnChange
    void onOrderChange(ChangeStreamContext ctx) {
        Order order = (Order) ctx.getFullDocument(Order.class).orElse(null);
        events.add(new CapturedEvent(
                ctx.getOperationType(),
                order,
                ctx.getCollectionName()
        ));
    }

    public List<CapturedEvent> getEvents() {
        return events;
    }

    public record CapturedEvent(
            OperationType operationType,
            Order fullDocument,
            String collectionName
    ) {}
}

Validation Errors

FlowWarden validates handler methods at startup. Here are the common errors:
ErrorCauseFix
”must have at least one handler method”@ChangeStream class has no @OnChange, @OnInsert, etc.Add at least one handler method.
”must have exactly one @OnChange method”Multiple @OnChange methods in the same class.Keep only one @OnChange.
”must have at most one @OnInsert method”Duplicate typed handler for the same operation.Keep only one per type.
”must have at most one @OnUpdate method, found 2”Two different methods have the same typed annotation.Keep only one method per annotation type. You can combine multiple annotations on a single method instead.
”uses a document-typed signature but documentType is Document.class”Handler uses (T doc, ...) but no concrete documentType is set.Set documentType = YourClass.class on @ChangeStream.
”has invalid signature”Method parameters don’t match any supported style.Use one of the three supported signatures.
”has unsupported return type”Return type is not void or Mono<Void>.Use void (imperative) or Mono<Void> (reactive).
“returns Mono but mode is IMPERATIVE”Mono<Void> handler in IMPERATIVE mode.Switch to void return type, or change flowwarden.default-mode to REACTIVE.
”returns void but mode is REACTIVE”void handler in REACTIVE mode.Switch to Mono<Void> return type, or change flowwarden.default-mode to IMPERATIVE.

See Also

@ChangeStream

The parent annotation that declares a Change Stream handler class.

Handler Signatures

Detailed reference for all supported method signatures.

ChangeStreamContext

The context object passed to every handler — access document, metadata, and operations.

@Filter

Server-side filtering to reduce the events reaching your handlers.