Skip to main content
@ChangeStream is the natural way to declare a stream when its shape is known at compile time. It stops being enough when the stream catalog lives outside the JVM: a YAML file an operator edits, a table in a configuration database, a feature-flag service that turns streams on per tenant. Since 1.0.0-rc.6, the io.flowwarden.stream.registration package covers that case. A StreamDefinitionContributor bean receives a StreamRegistration at bootstrap and describes streams with a builder, StreamSpec. No annotated class is involved, and the contributed streams go through the same validation, defaults, and runtime as annotated ones.
Registration is bootstrap-only. Contributors run once, after every singleton bean has been created (annotated @ChangeStream classes included) and before the stream managers read the catalog. A contributed stream is fixed for the lifetime of the application context — there is no hot registration on a running instance.

Minimal example

That is the equivalent of:
The contributor is an ordinary Spring bean: inject whatever the handlers need and capture it in the lambdas.

Driving the catalog from configuration

The point of the API is that the number and shape of streams can come from data. A @ConfigurationProperties class bound to application.yml is the simplest source:
Change the YAML, restart: the stream set follows. Nothing is recompiled. The full runnable version of this pattern is sample 12-registration in flowwarden-examples, in both imperative and reactive flavours.

Annotation ↔ builder map

Every builder call maps 1:1 to an annotation, with the same defaults and the same fail-fast rules. See the StreamSpec reference for every signature.

Pipeline, filter and error handling

The three annotation capabilities that need code, not just attributes, take functional interfaces:
ErrorHandler is a public functional interface in io.flowwarden.stream.core: ErrorAction handle(Throwable ex, ChangeStreamContext<?> ctx). Resolution order between scoped and catch-all handlers, and the meaning of each ErrorAction, are the same as for @OnError.
A functional handler that throws — in either mode, Error included — is routed through @OnError / retry / DLQ resolution exactly like an annotated method. An ErrorHandler that itself throws falls back to RETHROW.

Validation and failure modes

A contributed stream is validated at bootstrap with the rules shared with the annotation path:
  • checkpoint, retry, and DLQ bounds;
  • collection resolution (collection, or a documentType annotated with @Document; the raw Document.class without a collection fails);
  • mongoTemplateRef must name a bean that is a MongoTemplate / ReactiveMongoTemplate;
  • handler mode must match the execution mode (imperative handlers on an imperative stream, reactive on reactive);
  • a filter cannot be combined with a typed handler on an operation without a fullDocument;
  • duplicate onError types or catch-alls are rejected.
A duplicate stream name — annotated vs contributed, or two contributors — fails application startup. So does a builder misuse: a second pipeline, a second filter, or a second handler for the same operation throws IllegalStateException from the builder itself.

When to stay on annotations

  • The stream needs a zone — not on StreamSpec yet.
  • The stream is one fixed unit of code with no external configuration: annotations stay shorter and are visible in the class.
  • You want the stream to appear in code search by its handler methods.
Both styles coexist in the same application and share the same catalog, actuator endpoints and metrics.