Skip to content

Tracking, updating documents

Fluxzero allows you to track changes to your document store using the @HandleDocument annotation. This enables handlers to react to document updates in real time — much like handling events.

@HandleDocument
void handle(UserAccount user) {
log.info("UserAccount {} was added or updated", user.getUserId());
}

Every time a document is (re)indexed, it receives a new message index (based on the timestamp of the update). Handlers annotated with @HandleDocument will observe these updates as they pass through the document log.

This makes @HandleDocument ideal for live processing, projecting the latest known state, or cache rebuilds.


@HandleDocument can also be used to update documents in place.

If the handler returns a newer revision of the document, Fluxzero will reindex and persist the result:

@HandleDocument("users")
UserAccount upgrade(UserAccount oldUser) {
return new UserAccount(oldUser.getId(), normalizeEmail(oldUser.getEmail()));
}

The returned value replaces the previous document (same ID), only if it has a higher @Revision.

A plain returned document retains all metadata from the exact stored version being handled, including through upcasting. Derived search indexes/exclusions are regenerated from the replacement. Tracking metadata such as the envelope’s $start and $end is not persisted as document metadata. Existing document timestamps remain in effect unless the returned type declares timestamp/end paths.

For an ordinary document, return a Message to explicitly replace the complete metadata map. Its payload still needs a higher revision; its message ID and timestamp do not override the document’s identity or indexed times. Use Metadata.empty() to remove all document metadata. Returning null directly retains the deletion behavior. Only payload and metadata participate in this write; other Message envelope fields are ignored.

A custom Serializer.deserializeMessages(...) implementation may buffer, reorder or split a document batch. If it constructs entirely new envelopes or decoded messages, call DocumentMessageReader.retainSource(decodedOutput, originalInput) for every corresponding output, keeping the original stored input before transformations. Standard input-envelope withData, withMetadata and withSegment operations preserve that source automatically. The helper transfers the original document bytes for metadata preservation; the custom serializer still owns ordinary message identity, revision and transport fields. See DocumentMessageReader.retainSource Javadoc for the complete extension contract.

@HandleDocument("users")
Message upgrade(UserAccount user) {
return new Message(user, Metadata.of("source", "migration"));
}

To delete a document from the store, return null:

@HandleDocument("users")
UserAccount upgrade(UserAccount user) {
return user.isTestUser() ? null : user;
}

Model documents have a narrower return contract: a handler can persist a schema upgrade, but business changes and deletion go through Model commands. For internal non-searchable document state, select source = DocumentSource.MODEL_STATE. See Testing Model migrations.

  • Auto-upcasting legacy documents
  • Filling or correcting derived fields
  • Cleaning up invalid or deprecated data
  • Running background migrations or rehydration jobs
  • Current-state analytics (use events when every change must be counted or logged)

The parameter selects what the handler observes:

DeclarationSubscription
@HandleDocument with TaskCanonical searchable Task node
@HandleDocument with Graph<Task>Task and its included descendants, in NONE, ASYNC and AWAIT
@HandleDocument(source = DocumentSource.MODEL_STATE) with TaskInternal maintained Task state, even without searchability
@HandleDocument(documentClass = Task.class)Explicit node type when it cannot be inferred
@HandleDocument("myCollection")An ordinary named document collection

A Graph handler observes its root and included descendants. It can observe a live Graph (NONE) or a stored projection (ASYNC/AWAIT). Updates may coalesce; use event-bound Graph handlers when you need history or every transition. A deleted root arrives as an empty Graph.

For subscription selection, registration and durable projection changes, see HandleDocument and Choosing Model and Graph queries.


Every node in a materialized Graph stores its own type and revision. When the Graph is read, the normal serializer applies each Model type’s ordinary @Upcast chain independently and lazily. There is no separate Graph upcaster and a child Model does not inherit the root’s revision.

If lazy read-time upcasting is sufficient, no document handler is needed. To durably rematerialize the evolved node JSON for search, return the complete Graph from a dedicated handler:

@HandleDocument
Graph<Project> rematerialize(Graph<Project> graph) {
return graph;
}

Fluxzero migrates nodes whose type or revision has evolved, using their verified current canonical state. Return the complete, unchanged Graph; use Model commands for business changes. This works in NONE, ASYNC and AWAIT and keeps history and relationships unchanged. It does not copy stale business values from the handled Graph into newer state.

Node writes complete before the handler completes; affected projections refresh durably afterward. Verify new search paths after catch-up. Migration is idempotent per node, not atomic across the whole Graph. The consumer’s error policy determines how failures are recovered; use a Runtime that supports this migration contract.

For a deliberate migration of only the stored composition, select @HandleDocument(graphMigration = GraphMigrationTarget.PROJECTION). This leaves canonical nodes unchanged and has no storage effect in NONE; a later rebuild from unchanged sources can restore old paths. See HandleDocument for concurrency, completion and custom-serializer contracts.


Want to upgrade all existing documents in a collection? Combine @HandleDocument with replays:

  1. Create a handler method that returns upgraded documents
  2. Increment the @Revision on the model class
  3. Attach a custom consumer with @Consumer(minIndex = 0, ...)
  4. Deploy temporarily until migration completes

This is a robust and rapid way to reindex, clean, or refactor your stored documents in-place.

@Consumer(name = "reindex-users", minIndex = 0)
class ReindexUsers {
@HandleDocument("users")
UserAccount upgrade(UserAccount legacyUser) {
return fixLegacyState(legacyUser);
}
}

Once the transformation is complete, the handler can be safely removed.


© 2026 Fluxzero