Skip to content

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.

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();

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.

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);

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.

A typed, functional ID is the default identity form. Fluxzero also supports integrations that supply another form.

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);

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.

Use @Alias when an independently stored model must also be addressable by a current business or external identifier:

@Model
record Customer(
@EntityId CustomerId customerId,
@Alias(prefix = "email:") String email,
CustomerDetails details) {
}
Customer customer = Fluxzero.loadModel(
"email:info@example.com", Customer.class).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.

Use @EntityId(parentScoped = true) when a parent-owned child ID is intentionally unique only inside one parent:

@Model
record Line(
@EntityId(parentScoped = true) String lineId,
@Parent(value = Order.class, pathInParent = "lines") 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);

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 sourcing is the default:

@Model
record Order(@EntityId OrderId orderId, OrderStatus status) {
}

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.

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) {
}

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.

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:

@HandleEvent
void on(ItemReserved event, Order order, Graph<Inventory> 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 @EntityId name;
  • 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:

@HandleQuery
OrderView inspect(InspectLine query, LineItem line, Order order, Customer customer) {
// 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);

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.

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:

@HandleEvent
void organisationChanged(Graph<Organisation> graph) {
Graph<Organisation> before = 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.

Relations declared with @Parent can be traversed without turning the models back into one permanent consistency boundary:

Graph<Order> graph = 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);

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