Skip to main content
FlowWarden processes Change Stream events sequentially within each stream — one event at a time. This is a deliberate design choice that keeps checkpointing simple and guarantees event ordering. This page explains why, when it matters, and how to scale beyond it.

Threading model

One thread per stream

In imperative mode, each @ChangeStream runs on its own dedicated thread managed by Spring Data MongoDB’s DefaultMessageListenerContainer. Events are processed one at a time in order:
In reactive mode, the pipeline uses concatMap (not flatMap), which guarantees sequential processing per stream on Reactor’s scheduler threads.

Why sequential?

Three constraints make sequential processing the natural default:
  1. Checkpoint is a linear cursor — FlowWarden persists a single resume token per stream. If events were processed out of order, a crash after checkpointing event 3 (but before event 2 completes) would lose event 2 forever.
  2. Event ordering matters — MongoDB Change Streams deliver events in oplog order. An update to a document should be processed after its insert, not before.
  3. Simplicity — sequential processing eliminates race conditions, makes handlers easy to reason about, and avoids the need for complex coordination.

When sequential is enough

For most use cases, sequential processing is not the bottleneck:
  • A single handler typically processes thousands of events per second — MongoDB’s Change Stream delivery is usually the limiting factor, not the handler
  • Multiple @ChangeStream classes watching different collections run in parallel on separate threads
  • @Pipeline server-side filtering reduces the event volume before it reaches your handler

Virtual threads

Spring Boot 3.2+ supports virtual threads (Project Loom) via spring.threads.virtual.enabled=true. FlowWarden is compatible with virtual threads, but there are important considerations.

What changes with virtual threads

Avoid synchronized on handlers

Do not use synchronized on handler methods when virtual threads are enabled. This causes thread pinning — the virtual thread remains attached to its carrier thread during the entire synchronized block, blocking it from running other virtual threads.
In practice, synchronized on a handler is redundant: FlowWarden processes events sequentially within a single stream — there is never concurrent access to the same handler for the same stream. If you need cross-stream coordination, use a ReentrantLock instead.

Scaling with message brokers

When a single stream’s sequential throughput is not enough, the recommended pattern is to use FlowWarden as a reliable CDC consumer that fans out events to a message broker. The broker handles parallelism natively via partitions (Kafka) or competing consumers (RabbitMQ).

Why this pattern?

FlowWarden handles

  • Reliable Change Stream consumption
  • Checkpoint & resume on restart
  • Retry & DLQ for publish failures
  • Leader election (single consumer)

The broker handles

  • Parallel processing across N consumers
  • Message ordering per partition/key
  • Backpressure and flow control
  • Independent retry per consumer

Example with Kafka

Kafka consumers then process events in parallel, one consumer per partition:

Example with RabbitMQ

RabbitMQ consumers compete for messages, enabling parallel processing:

Design considerations