Loading models
Fluxzero.loadModel(...) loads one Model by identity. Fluxzero.loadGraph(...) provides a lazy Graph for reads that
also depend on related Models or history.
Load one Model
Section titled “Load one Model”Loading a Model does not load its parents or children.
With an OrderId extends Id<Order>, the ID supplies the Model type:
Order order = Fluxzero.loadModel(new OrderId("order-123")).get();val order: Order? = Fluxzero.loadModel(OrderId("order-123")).get()get() returns the current value, or null when absent. Inside historical Model-event handling, loads can follow
the event’s boundary; see temporal Graphs when you need the state of that event or explicitly now.
Follow relationships with a Graph
Section titled “Follow relationships with a Graph”Load a Graph<T> when the answer also depends on related state, history or a transition:
Graph<Order> graph = Fluxzero.loadGraph(orderId);Order order = graph.get();Graph<Order> previous = graph.previous();List<LineItem> lines = graph.childModels("lines", LineItem.class);val graph: Graph<Order> = Fluxzero.loadGraph(orderId)val order: Order? = graph.get()val previous: Graph<Order>? = graph.previous()val lines: List<LineItem> = graph.childModels("lines", LineItem::class.java)get() reads the node, previous() opens its preceding revision, and childModels(...) reads related values.
Exact historical values require retained event-sourced history. Use isPresent() when you need to distinguish an absent node
before reading it.
Graph navigation is lazy: creating a typed Graph does not load the whole tree. Once its read boundary is established,
later navigation stays at that boundary. To open a new current view, use current().
The temporal Graph guide explains history and navigation;
Graph lists identity, revision, traversal and update methods.
Identity options
Section titled “Identity options”A typed, functional ID is the default identity form. Fluxzero also supports integrations that supply another form.
Use an existing repository key
Section titled “Use an existing repository key”A typed load normally converts a functional ID and applies the Model’s ID prefixes and postfixes. When you already
have the complete repository key, pass false to use it unchanged. This also works for a full parent-scoped key.
Graph<Order> current = Fluxzero.loadCurrentGraph(graph.id(), Order.class, false);val current = Fluxzero.loadCurrentGraph(graph.id(), Order::class.java, false)The same option is available on loadModel, loadGraph and loadModels. It controls ID conversion and affixes,
not aliases: both modes try the primary identity first and then resolve an alias. No prefix is removed from a typed
ID’s existing string representation. For an existing Graph, graph.current() is usually simpler and also retains
its owning repository and namespace. Exact historical and verified-document reads keep their separate contracts.
Alternative model identifiers
Section titled “Alternative model identifiers”Use @Alias when an independently stored model must also be addressable by a current business or external identifier:
@Modelrecord Customer( @EntityId CustomerId customerId, @Alias(prefix = "email:") String email, CustomerDetails details) {}
Customer customer = Fluxzero.loadModel( "email:info@example.com", Customer.class).get();@Modeldata class Customer( @EntityId val customerId: CustomerId, @Alias(prefix = "email:") val email: String, val details: CustomerDetails)
val customer: Customer? = Fluxzero.loadModel("email:info@example.com", Customer::class.java).get()Fluxzero stores the complete alias set atomically with every model transition. When the annotated value changes, the old alias stops resolving after the commit; logical or physical deletion removes the current aliases as well.
Aliases for independent models are global identities and must be unique across those models. A primary model ID is always resolved before an equal alias. Use parent-scoped primary identities when a child ID is meaningful only within its parent.
Parent-scoped model identifiers
Section titled “Parent-scoped model identifiers”Use @EntityId(parentScoped = true) when a parent-owned child ID is intentionally unique only inside one parent:
@Modelrecord Line( @EntityId(parentScoped = true) String lineId, @Parent(value = Order.class, pathInParent = "lines") OrderId orderId) {}@Modeldata class Line( @EntityId(parentScoped = true) val lineId: String, @Parent(value = Order::class, pathInParent = "lines") val orderId: OrderId)The field remains "one"; Fluxzero only scopes the persisted model identity. Automatic apply routing selects the
deepest non-null parent relation present in the payload. Within a loaded graph,
order.find("one", Line.class) therefore keeps using the functional child ID. An explicit direct load includes the
parent identity and type:
Graph<Line> line = Fluxzero.loadGraph( orderId, Order.class, "one", Line.class);val line: Graph<Line> = Fluxzero.loadGraph(orderId, Order::class.java, "one", Line::class.java)All non-null parents must resolve to one unambiguous nearest parent. Parent-scoped identity is unsuitable for a model that moves while retaining its identity, because changing the parent changes the persisted identity.
Open graph.current() for the same Model without replacing the original Graph. Outside mutations it pins a new
current boundary during the call and captures pending batch changes; inside a mutation it shares that attempt’s
boundary, staged values and read dependencies. It retains exact identity and owning repository/namespace, including
affixes and parent scope. A moved node gets the parents at that selected boundary; a deleted Model is empty. Filters,
mapped values, uncommitted edits on the source Graph and response context are not copied. Reapply presentation filters.
Unknown nodes and custom repositories without the required current-read capability fail explicitly.
See Compose and update related Models for examples, including direct companion loads by parent ID and Model type.
Event-sourced loading
Section titled “Event-sourced loading”Event sourcing is the default:
@Modelrecord Order(@EntityId OrderId orderId, OrderStatus status) {}@Modeldata class Order(@EntityId val orderId: OrderId, val status: OrderStatus)Fluxzero first checks the model cache, then an applicable snapshot or checkpoint, and finally reads only the required
suffix of that model’s stream. Events are replayed through their @Apply methods.
flowchart LR
LOAD["loadModel(id)"] --> CACHE{"Current cache entry?"}
CACHE -->|fresh| RETURN["Return current model"]
CACHE -->|missing or stale| SNAPSHOT["Load snapshot/checkpoint"]
SNAPSHOT --> EVENTS["Load missing model events"]
EVENTS --> APPLY["Replay @Apply methods"]
APPLY --> RETURN
Each independently stored child has a short stream of its own. Updating one line item does not replay the lifetime history of the order, inventory model or its siblings.
A newly evaluated Model operation with injected Graph dependencies establishes its initial read boundary at storage, even when the root Model is already cached. A root’s cached revision alone cannot prove that no child was added, removed or moved. Relationships remain lazy, and once selected the boundary stays pinned through the evaluation; RETRY reevaluates at a new boundary, while historical event handling keeps its event boundary. Nested assertions and interceptor replacements conservatively establish this boundary too because they can introduce Graph parameters. Plain Model operations without these dependencies retain their cache fast path. Pending writes and cache entries belong to the owning repository family and namespace, never to another application sharing a cache or active batch.
Background cache replay runs in that repository’s configured application context, not the context of its first caller.
Document-based loading
Section titled “Document-based loading”Select document persistence to load the current model directly from its synchronously maintained document:
@Model(persistence = ModelPersistence.DOCUMENT)record Country(@EntityId CountryId countryId, CountryDetails details) {}@Model(persistence = [ModelPersistence.DOCUMENT])data class Country(@EntityId val countryId: CountryId, val details: CountryDetails)This changes only the normal load strategy. Applied updates are still stored and published as events unless
eventPublication or publicationStrategy says otherwise. Its internal current-state document completes with the Model commit. Enable searchable separately when node or Graph queries are required.
Document-based models participate in the same durable model-update tracking as event-sourced models, so caches in other SDK instances are invalidated or refreshed after remote commits.
Model parameters in message handlers
Section titled “Model parameters in message handlers”Every selected message handler can inject directly addressed models and their ancestors. When an event or notification was produced by a model commit, Fluxzero pins those loads to that action’s persisted state boundary:
@HandleEventvoid on(ItemReserved event, Order order, Graph<Inventory> inventory) { // order and inventory are exactly as they existed for this event}@HandleEventfun on(event: ItemReserved, order: Order, inventory: Graph<Inventory>) { // order and inventory are exactly as they existed for this event}Parameter injection uses the ID properties in the event:
- a property matching the model’s
@EntityIdname; - otherwise one unique typed
Id<Model>; @Association("sourceId")when two IDs refer to the same model type.
Both T and Graph<T> are supported. A deleted target can be injected as an empty graph; a non-null bare T matches
only when the model exists. @Association first checks metadata unless excludeMetadata = true.
An ordinary event without a Model-commit boundary resolves directly addressed Models at one current pinned boundary. Complete graph-change subscriptions still require durable Model commit metadata because they discover every affected root rather than only IDs addressed by the event.
Parents, grandparents and further ancestors can be injected without repeating their IDs:
@HandleQueryOrderView inspect(InspectLine query, LineItem line, Order order, Customer customer) { // query only needs the typed LineItemId; relations resolve Order and Customer}@HandleQueryfun inspect(query: InspectLine, line: LineItem, order: Order, customer: Customer): OrderView { // query only needs the typed LineItemId; relations resolve Order and Customer}The child parameter may be omitted. A typed child ID in the payload is enough to anchor graph traversal when the
selected handler only requests an ancestor. Use @Association("billingLines") to select the relation path if several
ancestors of the requested type are reachable.
Later cache entries never leak into historical event handling. Earlier STORE_ONLY model transitions are included
because exactness is based on the committed model commit, not only on the globally published event stream.
Ordinary loadGraph(...) calls made while handling a message inherit that same coherent read boundary. This is usually
what event and notification handlers need. Use loadCurrentGraph(...) when current intent is deliberately required:
after a synchronous nested command, or in a scheduling reconciler deciding which deadlines still belong despite
receiving an old event. For example:
Fluxzero.sendCommandAndWait(new ReserveItem(itemId));Graph<Item> current = Fluxzero.loadCurrentGraph(itemId);Fluxzero.sendCommandAndWait<Any?>(ReserveItem(itemId))val current: Graph<Item> = Fluxzero.loadCurrentGraph(itemId)This explicit escape prevents a handler from accidentally mixing the event-visible past with unrelated latest state.
Commands, queries, schedules, results, errors, metrics, documents, custom messages and web handlers use the same parameter rules. They share one current handler load context: event-sourced targets use its pinned repository boundary, while document-loaded targets remain current-only direct-document reads. Handler selection itself performs no repository I/O.
Handling complete graph changes
Section titled “Handling complete graph changes”Use one unqualified Graph<T> parameter to subscribe to every durable change of that root or one of
its descendants. Optional Message and Metadata context parameters may appear in any order:
@HandleEventvoid organisationChanged(Graph<Organisation> graph) { Graph<Organisation> before = graph.previous();}@HandleEventfun organisationChanged(graph: Graph<Organisation>) { val before: Graph<Organisation>? = graph.previous()}Fluxzero reads the affected model identities from the exact durable model-commit substep, so the published event does
not need to repeat the root ID. Each affected root is invoked once, even when several models in that graph changed in
one committed substep. graph.previous() is the graph immediately before that change, independent of model cache
depth when its inspected nodes have event-sourced history.
- creation supplies the new graph and no previous graph;
- deletion supplies an empty current graph and the complete deleted graph through
previous(); - moving a child invokes the handler for both the old and the new root;
@HandleNotification void changed(Graph<Organisation> graph)has the same graph semantics without regular consumer segmentation.
One handler object may contain multiple such methods for distinct root types. Fluxzero routes each affected root to
the matching typed Graph<T> method, so no per-type adapter objects are needed.
A handler that also declares the explicit event payload remains an ordinary payload handler. Its Graph<T> parameter
keeps the direct or ancestor-injection semantics described above.
Complete subscriptions select roots from the durable change targets and their current/previous parent relationships,
not from a unique ID property in the event payload. An event may therefore contain several typed IDs for the same
model (for example, sourceId and destinationId). Plain references do not create parent relationships: an unrelated
model change invokes no subscription for those IDs. An ordinary event without Model commit metadata also invokes none.
Explicit payload handlers retain their normal precedence and ambiguity checks; use @Association when their injected
Model or Graph must select one of several references. The same rules apply to @HandleNotification.
Cascaded logical deletions also notify the deleted child’s complete-change Graph<T> handler through the original domain event.
It receives an empty current graph and the previous value; no extra public technical event or parent-specific cleanup
handler is needed. Cascades retain their own internal change boundary. A child updated and then cascaded in one
commit can therefore produce both observations; surviving shared ancestors observe removal as well. An already
deleted ancestor is not notified twice for that deletion. The cascaded child’s previous() retains the state before its own deletion substep: its value is available, but
an ancestor already deleted in an earlier substep is no longer reachable. It does not rewind the whole commit.
Suppressed publication and physical erasure do not manufacture domain notifications. Normal delivery retry rules apply.
Need exact previous values? Keep EVENT_SOURCED enabled. DOCUMENT alone stores current state, not historical
document versions. Neither a before-boundary nor a larger cache can reconstruct an overwritten document durably.
This requirement applies to each Graph node whose exact historical value you require. Event sourcing plus DOCUMENT
retains history; ordinary snapshot/cache settings do not prune Model events automatically.
Historical Graph views normally use current DOCUMENT-only values when a historical revision is unavailable.
Use graph.strict(true) or fluxzero.model.graph.strict=true for exact history; see
Temporal Graphs for fallback behavior.
Loading a complete model graph
Section titled “Loading a complete model graph”Relations declared with @Parent can be traversed without turning the models back into one permanent consistency
boundary:
Graph<Order> graph = Fluxzero.loadGraph(orderId);val graph: Graph<Order> = Fluxzero.loadGraph(orderId)The source value remains lazy; the runtime pins one stateIndex when model state is first required and reconstructs
related nodes in batches on the first traversal. Every returned child is itself a graph with its own parent, root and history operations. Use
loadGraphAt(...) for an historical graph:
Graph<Order> historical = Fluxzero.get() .modelRepository() .loadGraphAt(orderId, stateIndex);val historical: Graph<Order> = Fluxzero.get() .modelRepository() .loadGraphAt(orderId, stateIndex)Graph loads have no caller-imposed depth or model-count limit by default. Pass explicit Graph.Options when an
operation intentionally accepts only a bounded graph; use a direct model load when the command needs only one model.
Returning Graph<T> from a message or web handler serializes the root and descendants placed through explicit
@Parent(pathInParent = "...") edges. Pathless relationships remain available through typed children(...) and
descendants(...), but do not invent a public JSON property name.
Use @GraphProperty for a response value derived from graph context rather than from one independently stored Model.
The method may receive the current graph or a typed ancestor graph. When many nodes share an already-batched lookup,
attach that response-only value once with graph.withContext(value) and read it inside the property method with
graph.context(ValueType.class). The context is shared by the immutable graph view and is never persisted as Model
state.
A current graph load inside an ordered tracking batch includes pending model state produced by earlier messages in the
same routing segment. This includes newly created nodes, changed values and @Parent moves. Moving an existing node
into the graph also brings along its durable descendants. loadGraphAt(...) is deliberately different: it always
returns the exact requested historical boundary and never overlays pending batch state.
For a worked before/after example and the distinction between root revisions and descendant changes, see Temporal Graphs.
© 2026 Fluxzero