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@OnChangeCatch-all for any operation not handled by a typed annotation
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 — invoked for every operation that is not covered by a typed handler, including DROP and INVALIDATE
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());
    }
}
@ChangeStream(documentType = Order.class)
public class OrderHandler {

    @OnChange
    Mono<Void> handle(ChangeStreamContext<Order> ctx) {
        return Mono.fromRunnable(() ->
            System.out.println(ctx.getOperationType() + " on " + ctx.getCollectionName()));
    }
}
Rules:
  • Only one @OnChange method is allowed per @ChangeStream class.
  • The annotation takes no attributes — it is a pure catch-all.
  • Signature must use the CONTEXT_ONLY style — void handle(ChangeStreamContext ctx) or Mono<Void> handle(ChangeStreamContext ctx).
To handle a specific subset of operations, declare the corresponding typed handlers (e.g. @OnInsert and @OnUpdate) — either on separate methods or stacked on a single method delegating to a shared private helper. See Combining Annotations on the Same Method below.

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());
}
@OnInsert
Mono<Void> onInsert(Order doc, ChangeStreamContext<Order> ctx) {
    return Mono.fromRunnable(() ->
        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());
}
@OnUpdate
Mono<Void> onUpdate(ChangeStreamContext<Order> ctx) {
    return Mono.fromRunnable(() ->
        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 the recommended way to scope a handler to a specific subset of operation types — typed annotations make the intent explicit, and they take priority over @OnChange if a catch-all is also declared in the same class.
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.
@OnChange is a catch-all only — it cannot be narrowed to specific operation types. To target a subset (e.g. INSERT and UPDATE only), declare typed handlers that delegate to a shared helper:
@OnInsert
@OnUpdate
void handleWrite(ChangeStreamContext<Order> ctx) {
    onWrite(ctx);
}

private void onWrite(ChangeStreamContext<Order> ctx) {
    // shared logic for INSERT and UPDATE
}
This keeps the dispatch decision in annotations (visible to the framework) and the logic in plain Java, instead of hiding the operation filter inside an annotation attribute.

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());
    }
}
@ChangeStream(name = "typed-order-capture", documentType = Order.class)
public class TypedOrderHandler {

    @OnInsert
    Mono<Void> onInsert(Order doc, ChangeStreamContext<Order> ctx) {
        return Mono.fromRunnable(() ->
            log.info("New order: {}", doc.getId()));
    }

    @OnUpdate
    Mono<Void> onUpdate(ChangeStreamContext<Order> ctx) {
        return Mono.fromRunnable(() ->
            log.info("Order updated: {}", ctx.getDocumentKey()));
    }

    @OnDelete
    Mono<Void> onDelete(ChangeStreamContext<Order> ctx) {
        return Mono.fromRunnable(() ->
            log.info("Order deleted: {}", ctx.getDocumentKey()));
    }

    @OnChange
    Mono<Void> onFallback(ChangeStreamContext<Order> ctx) {
        return Mono.fromRunnable(() ->
            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.