> ## Documentation Index
> Fetch the complete documentation index at: https://docs.flowwarden.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Event Handlers

> Route Change Stream events to specific methods with @OnChange, @OnInsert, @OnUpdate, @OnDelete, and @OnReplace.

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`](/reference/change-stream) class.

## Overview

There are two types of handler annotations:

| Type        | Annotations                                         | Purpose                                                       |
| ----------- | --------------------------------------------------- | ------------------------------------------------------------- |
| **Generic** | `@OnChange`                                         | Catch-all for any operation not handled by a typed annotation |
| **Typed**   | `@OnInsert`, `@OnUpdate`, `@OnDelete`, `@OnReplace` | Routes 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`

```mermaid theme={null}
flowchart LR
    A["INSERT event"] --> B{"@OnInsert<br/>found?"}
    B -->|"Yes"| C["call @OnInsert"]
    B -->|"No"| D{"@OnChange<br/>exists?"}
    D -->|"Yes"| E["call @OnChange"]
    D -->|"No"| F(["event ignored"])
```

<Note>
  Typed handlers always take priority. `@OnChange` is only invoked for operation types that have **no dedicated typed handler** in the class.
</Note>

## @OnChange

Generic handler for all Change Stream event types. Acts as a catch-all when no typed handler matches.

<CodeGroup>
  ```java Imperative theme={null}
  @ChangeStream(documentType = Order.class)
  public class OrderHandler {

      @OnChange
      void handle(ChangeStreamContext<Order> ctx) {
          System.out.println(ctx.getOperationType() + " on " + ctx.getCollectionName());
      }
  }
  ```

  ```java Reactive theme={null}
  @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()));
      }
  }
  ```
</CodeGroup>

**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)`.

<Tip>
  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](#combining-annotations-on-the-same-method) below.
</Tip>

## Typed Handlers

### @OnInsert

Called when a new document is inserted into the collection.

<CodeGroup>
  ```java Imperative theme={null}
  @OnInsert
  void onInsert(Order doc, ChangeStreamContext<Order> ctx) {
      log.info("New order: {}", doc.getId());
  }
  ```

  ```java Reactive theme={null}
  @OnInsert
  Mono<Void> onInsert(Order doc, ChangeStreamContext<Order> ctx) {
      return Mono.fromRunnable(() ->
          log.info("New order: {}", doc.getId()));
  }
  ```
</CodeGroup>

### @OnUpdate

Called when an existing document is updated.

<CodeGroup>
  ```java Imperative theme={null}
  @OnUpdate
  void onUpdate(ChangeStreamContext<Order> ctx) {
      log.info("Order updated: {}", ctx.getDocumentKey());
  }
  ```

  ```java Reactive theme={null}
  @OnUpdate
  Mono<Void> onUpdate(ChangeStreamContext<Order> ctx) {
      return Mono.fromRunnable(() ->
          log.info("Order updated: {}", ctx.getDocumentKey()));
  }
  ```
</CodeGroup>

<Tip>
  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.
</Tip>

### @OnDelete

Called when a document is deleted.

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

<Warning>
  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.
</Warning>

### @OnReplace

Called when a document is replaced entirely (e.g. via `MongoTemplate.save()` on an existing document).

```java theme={null}
@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:

```java theme={null}
@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.

<Tip>
  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.
</Tip>

<Warning>
  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.
</Warning>

<Accordion title="Handling a subset of operations">
  `@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:

  ```java theme={null}
  @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.
</Accordion>

## Supported Signatures

### Parameter Styles

Typed handler methods (`@OnInsert`, `@OnUpdate`, `@OnDelete`, `@OnReplace`) support three parameter styles:

| Style                  | Signature                                   | Description                                                   |
| ---------------------- | ------------------------------------------- | ------------------------------------------------------------- |
| `CONTEXT_ONLY`         | `handle(ChangeStreamContext<T> ctx)`        | Access context; get the document via `ctx.getFullDocument()`. |
| `DOCUMENT_ONLY`        | `handle(T doc)`                             | Receive the deserialized document directly.                   |
| `DOCUMENT_AND_CONTEXT` | `handle(T doc, ChangeStreamContext<T> ctx)` | Both the document and the context.                            |

`@OnChange` only supports `CONTEXT_ONLY`.

<Warning>
  `DOCUMENT_ONLY` and `DOCUMENT_AND_CONTEXT` signatures require a concrete `documentType` on `@ChangeStream` (not `Document.class`). Otherwise, startup fails with a validation error.
</Warning>

### Return Types — Mode Exclusivity

Handler methods must use the return type that matches the configured `flowwarden.default-mode`:

| Mode         | Required Return Type | Description                                                |
| ------------ | -------------------- | ---------------------------------------------------------- |
| `IMPERATIVE` | `void`               | Blocking handler. `Mono<Void>` is **rejected** at startup. |
| `REACTIVE`   | `Mono<Void>`         | Non-blocking handler. `void` is **rejected** at startup.   |

<Warning>
  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.
</Warning>

This gives the following full signature matrix:

<Tabs>
  <Tab title="Imperative (void)">
    | Style                  | Signature                                        |
    | ---------------------- | ------------------------------------------------ |
    | `CONTEXT_ONLY`         | `void handle(ChangeStreamContext<T> ctx)`        |
    | `DOCUMENT_ONLY`        | `void handle(T doc)`                             |
    | `DOCUMENT_AND_CONTEXT` | `void handle(T doc, ChangeStreamContext<T> ctx)` |
  </Tab>

  <Tab title="Reactive (Mono)">
    | Style                  | Signature                                              |
    | ---------------------- | ------------------------------------------------------ |
    | `CONTEXT_ONLY`         | `Mono<Void> handle(ChangeStreamContext<T> ctx)`        |
    | `DOCUMENT_ONLY`        | `Mono<Void> handle(T doc)`                             |
    | `DOCUMENT_AND_CONTEXT` | `Mono<Void> handle(T doc, ChangeStreamContext<T> ctx)` |
  </Tab>
</Tabs>

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.

<CodeGroup>
  ```java Imperative theme={null}
  @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());
      }
  }
  ```

  ```java Reactive theme={null}
  @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()));
      }
  }
  ```
</CodeGroup>

### Combined Annotations with @Filter

From the `sample-spring-mvc` module — a handler that reacts to both INSERT and UPDATE with a client-side filter:

```java theme={null}
@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");
    }
}
```

<Note>
  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.
</Note>

### Minimal — Single @OnChange

The simplest form: one handler for all event types.

```java theme={null}
@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:

```java theme={null}
@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:

| Error                                                                | Cause                                                              | Fix                                                                                                        |
| -------------------------------------------------------------------- | ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- |
| "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

<CardGroup cols={2}>
  <Card title="@ChangeStream" icon="satellite-dish" href="/reference/change-stream">
    The parent annotation that declares a Change Stream handler class.
  </Card>

  <Card title="Handler Signatures" icon="signature" href="/reference/handler-signatures">
    Detailed reference for all supported method signatures.
  </Card>

  <Card title="ChangeStreamContext" icon="circle-info" href="/reference/change-stream-context">
    The context object passed to every handler — access document, metadata, and operations.
  </Card>

  <Card title="@Filter" icon="filter" href="/reference/filter">
    Server-side filtering to reduce the events reaching your handlers.
  </Card>
</CardGroup>
