Skip to content

Core concepts

Fluxzero lets you build application behavior around messages. A command asks for a change, a query asks for information, and an event tells other parts of the application what happened. Your Java or Kotlin code handles those messages; Fluxzero takes care of their delivery and the storage of your Models.

Let’s build one small behavior: remember a task and mark it complete. The examples below belong together. Java uses Lombok’s @With for immutable copies; Kotlin uses copy. Put public Java types in separate files.

A Model is something your application remembers and changes over time. A task needs an identity, a name and a completion status:

@Model
public record Task(@EntityId TaskId taskId, String name,
@With boolean completed) {}
public final class TaskId extends Id<Task> {
public TaskId(String id) { super(id); }
}

@EntityId identifies the task. Id<Task> tells Fluxzero which Model that ID belongs to. We can now address one task without loading every task in the application.

A command names an action. Its @Apply method returns the Model state that should result:

public record CreateTask(TaskId taskId, String name) {
@Apply
Task apply() { return new Task(taskId, name, false); }
}
public record CompleteTask(TaskId taskId) {
@Apply
Task apply(Task task) { return task.withCompleted(true); }
}

Send the action through Fluxzero:

var taskId = new TaskId("t1");
Fluxzero.sendCommandAndWait(new CreateTask(taskId, "Read the guide"));
Fluxzero.sendCommandAndWait(new CompleteTask(taskId));

Fluxzero finds the task from the typed ID, applies the change and stores it. There is no forwarding command handler to write. Register these command types in your application; Spring discovers them within its configured scan. TestFixture, used below, discovers these payload-side applies automatically.

In a real task app, you can require a nonblank name with @NotBlank and check who may complete a task with @AssertLegal. These checks happen before the change is stored. See validation and Model updates.

By default, a Model keeps its accepted changes as events and reconstructs its state from them. This is event sourcing. You can choose other storage behavior later without changing what a command means to your users.

A query asks for information. Its handler returns the answer, and Request<Task> declares the expected result type:

public record GetTask(TaskId taskId) implements Request<Task> {
@HandleQuery
Task handle() { return Fluxzero.loadModel(taskId).get(); }
}
Task task = Fluxzero.queryAndWait(new GetTask(new TaskId("t1")));

This query loads a known task. To find tasks by their contents, enable search and use the Model query APIs.

Handlers are methods that respond to messages. The annotation describes which kind:

AnnotationResponds to
@HandleCommandAn action that needs explicit handling, such as calling an external service
@HandleQueryA request for information
@HandleEventSomething that has happened
@HandleGet, @HandlePostAn HTTP request
@HandleScheduleA scheduled message becoming due

The GetTask handler lives on its payload and runs locally by default. Separate handler objects are useful for reacting to several message types. Register those objects with your application, or let Spring discover @Component classes. See configuration.

After an accepted Model change, Fluxzero normally publishes the applied command payload as an event. CompleteTask as a command asks to complete the task; the same payload as an event tells other handlers that the change happened.

For example, an activity logger can react to completion without being part of the command:

@HandleEvent
void on(CompleteTask event, Task task) {
log.info("Completed task: {}", task.name());
}

Here log is your application’s logger. Fluxzero injects the task’s state at that event. Other consumers can react independently, for example by sending a notification.

An HTTP endpoint is a handler too:

@HandleGet("/hello")
String hello() { return "Hello, world!"; }

An endpoint can send commands or queries to expose your app’s behavior. Scheduled messages use the same idea: schedule a reminder for tomorrow, then handle it with @HandleSchedule when due.

TestFixture runs these behaviors locally, without a database or platform mocks. Create a task, complete it, and check its state through the query:

var taskId = new TaskId("t1");
TestFixture.create()
.givenCommands(new CreateTask(taskId, "Read the guide"), new CompleteTask(taskId))
.whenQuery(new GetTask(taskId))
.expectResult(new Task(taskId, "Read the guide", true));

Tests can also check rejected actions, outgoing messages, HTTP responses and scheduled work.

When tasks belong to projects, keep their independent identities and connect them with @Parent. A Graph lets you navigate those relationships and read a project together with its tasks. The to-do tutorial builds that relationship step by step, along with validation, authorization, queries and reminders. The developer guides cover individual capabilities.


© 2026 Fluxzero