Annotation Interface Model
Prefer separate models for new domain state. Each model keeps its own history as relationships change; a model
can move to another parent or gain additional relationships without transferring a containing root's history.
Several models can still participate in one atomic operation through Graph.
Unlike an embedded member in a legacy Aggregate, a model is its own persistence and lifecycle boundary.
Loading or updating it does not
require loading a parent, sibling, child, or an artificial aggregate root. A model may still contain embedded
entities declared with @Member; those members share the model's stream, cache, search document,
snapshots, and lifecycle.
Choose this boundary from domain lifecycle first: state that can be created, changed, retained, deleted, or whose
history matters independently is a separate model, even when it is normally displayed in a parent's collection.
Connect such a child with @Parent. A meaningful domain identity is strong evidence for that boundary,
not an additional gate: an independently living child may use a globally unique ID or a
parent-scoped ID. Collection shape, searchability, storage format, update frequency,
and convenient embedding do not make independently living state a Member.
Model identity is the repository representation of its @EntityId. Applications can use a typed
Id, annotation-level prefix/postfix affixes, or both to isolate otherwise equal functional identifiers.
An @Apply method targets a model by returning that model. Returning null deletes the targeted
model while retaining the applied event according to the configured publication settings. Returning void is
invalid for model applies because it does not identify a stored result. Legacy mutable aggregate applies remain
supported.
Persistence
persistence() makes the durable representations and authoritative load path explicit. Event-sourced models
are reconstructed from their model stream, optionally from a snapshot. Adding ModelPersistence.DOCUMENT
maintains internal current state; when event sourcing is absent, that current state is authoritative. Event storage
and publication remain independent and are controlled by eventPublication(),
publicationStrategy(), and per-apply overrides. Internal component documents used for Graph composition are
likewise orthogonal and never change the selected load path.
Example
@Model
public record Product(@EntityId ProductId productId, ProductDetails details) {
@Apply
Product rename(RenameProduct command) {
return new Product(productId, details.withName(command.name()));
}
}
Here ProductDetails is an immutable business value with a copy method such as Lombok's generated
withName. Use a cohesive details value even when the only descriptive field is a name; keep identity,
relationships and simple status distinct. A plain details value needs neither @Model nor Member.
A focused rename command may carry a scalar while replacing only that field in the existing details.
An update may instead create or update the model from a payload-side @Apply. When both sides define an
applicable apply, Fluxzero applies the payload first and invokes the model method against that intermediate state.
This lets one instance method consistently enforce model-owned behavior for both creation and later updates.
Persistence and searchability are separate choices. Enable searchable() to query this Model and its
composed descendants; SearchSettings.includeDescendants() limits a root's Graph scope. An explicit
Parent.pathInParent() only describes composition, never storage. The default Graph mode retains separate
indexed node documents and composes them when read. Choose GraphProjectionMode.ASYNC or
GraphProjectionMode.AWAIT to additionally store the composed Graph.
Model declarations are indexed at compilation by ModelTypeProcessor. Enable SDK annotation processing
(Kotlin: kapt) in each contract module and preserve ModelTypes.INDEX when packaging. This index discovers
classes without registering message handlers; the optional serialization @RegisterType is not required.
Contract JARs built before this index was introduced must be rebuilt for cold discovery. A shaded JAR must append
all contributing Model indexes rather than retain only one.
Identified abstract/interface contracts remain discoverable; identity-less abstract/interface inheritance templates
are not standalone Models and are omitted from the runtime catalog.
- See Also:
-
Optional Element Summary
Optional ElementsModifier and TypeOptional ElementDescriptionControls whether applies producing this model may be exposed as automatic command handlers.booleanWhether the latest model state should be stored in the shared application cache.intNumber of older model versions retained in the shared cache.intFrequency at which intermediate states are checkpointed within one reconstruction session.Controls when model changes are committed and whether completion-phase commits may run concurrently.Conflict handling for this model when an apply does not provide an explicit override.Controls whether an applied update produces an event, including unchanged results.Optional materialization of a complete composed Graph, configured independently from search activation.booleanWhether unknown events should be ignored while reconstructing an event-sourced model.intTarget number of newest snapshots retained for this model.Stable logical name of this Model in persisted Model metadata.Durable representations and authoritative load path for this Model.Controls whether applied events are stored, published, or both.booleanWhether this Model independently activates search.Per-node index settings and the scope of Graph queries rooted at this Model.intNumber of stored model events between snapshots.
-
Element Details
-
name
String nameStable logical name of this Model in persisted Model metadata.The default is the simple name of the concrete Model class. This name deliberately does not contain the Java package, so moving or renaming a class remains possible by retaining its previous logical name explicitly. Applications sharing one Runtime namespace can prepend an application-scoped prefix with
fluxzero.model.namePrefix; the prefix is concatenated literally and should therefore include any desired separator; prefixbillingand nameInvoicebecomebillingInvoice.This is a durable identity. Changing it for an existing Model creates a different Model type and requires an application-managed data transition.
- Default:
""
-
conflictPolicy
ModelConflictPolicy conflictPolicyConflict handling for this model when an apply does not provide an explicit override.ModelConflictPolicy.DEFAULTinherits the application policy, which defaults toModelConflictPolicy.RETRYfor updates and creations alike, independently of the defaults version. Factory compatibility is checked again after retry; retry does not implicitly turn a factory into an upsert.- Default:
DEFAULT
-
automaticHandling
AutomaticModelHandling automaticHandlingControls whether applies producing this model may be exposed as automatic command handlers.- Default:
DEFAULT
-
persistence
ModelPersistence[] persistenceDurable representations and authoritative load path for this Model.The set must contain at least one unique value. When
EVENT_SOURCEDis present, the event stream is authoritative. OtherwiseDOCUMENTis authoritative. To useGraph.previous()for historical values, keepEVENT_SOURCEDenabled:DOCUMENTalone stores current state, not previous versions. AddingDOCUMENTto event sourcing preserves history; replacing event sourcing withDOCUMENTdoes not.This setting does not suppress storing or publishing events produced by
Applymethods. A state-changing event-sourced Model apply must store its reconstructing event; aPUBLISH_ONLYorNEVERtransition that would change state is rejected before commit. A publish-only no-op remains a valid domain notification when publication is explicitly set toALWAYS.- Default:
{EVENT_SOURCED}
-
ignoreUnknownEvents
boolean ignoreUnknownEventsWhether unknown events should be ignored while reconstructing an event-sourced model.- Default:
false
-
snapshotPeriod
int snapshotPeriodNumber of stored model events between snapshots. The default0disables periodic snapshots. Enable only to bound measured replay work; snapshots do not create a searchable current document.- Default:
0
-
maxSnapshotCount
int maxSnapshotCountTarget number of newest snapshots retained for this model. Any negative value retains all periodic snapshots;0is treated as1. This does not enable snapshots:snapshotPeriod()must be positive. Unlimited retention does not prevent explicit physical erasure and does not change event-history retention. Enable negative values only after upgrading the Runtime to support unlimited Model snapshot retention. Concurrent Runtime writes may retain more; this is a cleanup target, not an exact count at commit completion. A later bounded snapshot write also trims earlier excess. An idle Model may retain excess until another such write; snapshot storage still completes before the commit is acknowledged.- Default:
1
-
cached
boolean cachedWhether the latest model state should be stored in the shared application cache.Models participating in a commit may additionally be retained in a commit-local cache until the commit completes. A document revision alone is not a namespace snapshot: simple DOCUMENT writes verify that revision when an additional transactional read first needs a shared boundary; complex contexts verify before evaluation.
- Default:
true
-
cachingDepth
int cachingDepthNumber of older model versions retained in the shared cache.-1retains all available cached versions;0retains only the latest version.Independent models retain one previous version by default so event handlers can compare the event-visible model with
Entity.previous(). Retaining an unbounded revision chain must be an explicit choice because model caches are expected to contain far more keys than aggregate caches.- Default:
1
-
checkpointPeriod
int checkpointPeriodFrequency at which intermediate states are checkpointed within one reconstruction session.Checkpoints avoid replaying the same prefix for repeated historical dependency loads. They are bounded by the reconstruction session and are not retained as document revisions.
- Default:
100
-
commitPolicy
ModelCommitPolicy commitPolicyControls when model changes are committed and whether completion-phase commits may run concurrently.The default value resolves from
fluxzero.model.commitPolicywhen present and otherwise usesModelCommitPolicy.ASYNC_AFTER_HANDLER_AWAIT_AFTER_BATCH. Independent models were introduced with this default, so it does not depend on the active defaults version.- Default:
DEFAULT
-
eventPublication
EventPublication eventPublicationControls whether an applied update produces an event, including unchanged results.Independent models default to
IF_MODIFIED, so a no-op apply does not create a model-stream or globally published event. UseALWAYSwhen an unchanged apply intentionally represents a domain event. This setting is evaluated beforepublicationStrategy().NEVERpermits eventless state changes only for document-loaded Models, not event-sourced Models. It does not suppress incoming command/webrequest logs, results or application logging.- Default:
IF_MODIFIED
-
publicationStrategy
EventPublicationStrategy publicationStrategyControls whether applied events are stored, published, or both.PUBLISH_ONLYmay mutate a document-loaded model. For an event-sourced model it may only publish an unchanged result, because otherwise the next reconstruction could not reproduce the committed state.- Default:
DEFAULT
-
searchable
boolean searchableWhether this Model independently activates search.Setting this to
truestarts a searchable scope at this Model and, by default, makes composed descendants effectively searchable as well. Setting it tofalsedoes not mean that this Model can never be searched; it only means that this Model does not activate search by itself. A searchable ancestor can still include it.searchable()starts a searchable scope.Parent.propagateSearch()controls whether that scope may propagate across a parent relationship. Set@Parent(propagateSearch = false)to block inherited searchability through one edge while leaving the child's own search activation and other parent relationships independent.SearchSettings.includeDescendants()can limit this Model's own searchable scope to the Model itself; it does not prevent the Model from participating in a broader searchable ancestor scope.Search activation does not require every property to participate in text search. Use
@SearchExcludeon individual properties or types to exclude them from text indexing and matching while retaining them in the stored and returned document.Whether the complete composed Graph is materialized is a separate choice controlled by
graphProjection(). Its default mode isNONE: Fluxzero keeps the separate indexed node documents and composes Graph results live when queried, without storing a complete Graph document. UseASYNCorAWAITonly when a stored composed Graph is desired.- Default:
false
-
searchSettings
SearchSettings searchSettingsPer-node index settings and the scope of Graph queries rooted at this Model. Does not activate search.- Default:
@io.fluxzero.sdk.modeling.SearchSettings
-
graphProjection
GraphProjection graphProjectionOptional materialization of a complete composed Graph, configured independently from search activation. The defaultNONEstores no complete Graph document and composes searchable Graphs live from the separate indexed nodes. ASYNC/AWAIT additionally maintain a stored composed Graph.- Default:
@io.fluxzero.sdk.modeling.GraphProjection
-