Skip to content

Models and Graphs

Fluxzero stores domain objects as independently addressable Models. A Graph reads a Model and its related Models at one boundary. For the other state mechanisms, see State in Fluxzero.

Each Model has its own identity, state, storage and lifecycle. A Task and Project can therefore be separate Models. An @Parent relation connects them without merging their storage or lifecycles. Updating or deleting the Task does not rewrite the Project or its other Tasks.

Changing the relation can move the Task without changing its identity. One Model action can still update several Models atomically.

A Graph is a connected view of Models at one read boundary. Starting from a Project, you can follow its Tasks. Starting from a Task, you can find its Project. The Graph keeps those reads consistent with one another, including when you inspect an earlier state.

flowchart LR
    T[Task Model] -. "@Parent relation" .-> P[Project Model]
    G["Graph<Project>"] --> P
    G --> T

The Models remain independently addressable. Use a Graph when a read depends on related Models or their state at an earlier boundary.

Give each Model a typed identity. On the child, @Parent declares the relationship and pathInParent gives that relationship a name when navigating from the parent:

@Model
record Project(@EntityId ProjectId projectId, ProjectDetails details) {}
@Model
record Task(
@EntityId TaskId taskId,
@Parent(pathInParent = "tasks") ProjectId projectId,
TaskDetails details,
boolean completed) {}
final class ProjectId extends Id<Project> {
ProjectId(String value) { super(value); }
}
final class TaskId extends Id<Task> {
TaskId(String value) { super(value); }
}

ProjectDetails and TaskDetails are ordinary immutable value objects. Their role is to keep descriptive fields, such as a name or title, together according to their domain meaning. Their declarations are omitted here because they have no independent lifecycle and add nothing to the Model–Graph relationship shown in this example.

@Parent records a Graph relation. The Task is still stored and loaded as its own Model; the Project does not need a List<Task> field.

Create each Model through a small action:

record CreateProject(ProjectId projectId, ProjectDetails details) {
@Apply Project apply() {
return new Project(projectId, details);
}
}
record AddTask(TaskId taskId, ProjectId projectId, TaskDetails details) {
@Apply Task apply(Project project) {
return new Task(taskId, project.projectId(), details, false);
}
}

The Project parameter makes adding a Task depend on an existing Project. After sending both actions, load the Project Graph and follow the named relation:

ProjectId projectId = new ProjectId("website");
TaskId taskId = new TaskId("write-homepage");
Fluxzero.sendCommandAndWait(new CreateProject(projectId, new ProjectDetails("Website")));
Fluxzero.sendCommandAndWait(new AddTask(taskId, projectId, new TaskDetails("Write the homepage")));
Graph<Project> project = Fluxzero.loadGraph(projectId);
List<Task> tasks = project.childModels("tasks", Task.class);

tasks now contains the Task, while Fluxzero.loadModel(taskId) can still load that same Task directly.

Make something a separate Model when it has its own creation, changes, history, retention or deletion. Use an ordinary value object when it changes only as part of its owner. Use @Member when the complete lifecycle deliberately belongs to one containing root.

Object shape is not the deciding factor. A small status can deserve its own Model; a large details object can remain a value. Ask who owns its lifecycle before choosing how convenient it is to embed.

Continue with:


© 2026 Fluxzero