@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
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:
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.
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 adocumentTypeannotated with@Document; the rawDocument.classwithout a collection fails); mongoTemplateRefmust name a bean that is aMongoTemplate/ReactiveMongoTemplate;- handler mode must match the execution mode (imperative handlers on an imperative stream, reactive on reactive);
- a
filtercannot be combined with a typed handler on an operation without afullDocument; - duplicate
onErrortypes or catch-alls are rejected.
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 onStreamSpecyet. - 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.