Updating entities
Fluxzero models domain state with entities that evolve by applying controlled updates. A group of related entities is called an aggregate (like an order and its line items). An aggregate is treated as a single unit for data changes.
Entities in an aggregate share a common root. The root is used as the entry point when updates are applied, ensuring that the aggregate’s consistency rules are preserved.
Defining the aggregate
Section titled “Defining the aggregate”To define the root of an aggregate, annotate it with @Aggregate:
@Aggregate@Builder(toBuilder = true)public record UserAccount(@EntityId UserId userId, UserProfile profile, boolean accountClosed) {}@Aggregate@Builder(toBuilder = true)data class UserAccount( @EntityId val userId: UserId, val profile: UserProfile, val accountClosed: Boolean)This class models an aggregate with fields like profile and accountClosed.
An aggregate is a root entity that groups its state together with any nested child entities (declared via @Member).
Like all entities, it defines its identity through a unique field annotated with @EntityId.
You’ll usually load an aggregate with Fluxzero.loadAggregate(...).
class UserId extends Id<UserAccount> { public UserId(String value) { super(value, "user-"); }}
@Aggregatepublic record UserAccount(@EntityId UserId userId) {}Entity<UserAccount> user = Fluxzero.loadAggregate(new UserId("1234"));class UserId(value: String) : Id<UserAccount>(value, "user-")
@Aggregatedata class UserAccount(@EntityId val userId: UserId)
val user = Fluxzero.loadAggregate(UserId("1234"))Applying updates to entities
Section titled “Applying updates to entities”Once an aggregate is loaded it can be updated. Here’s a basic example of a command handler applying a CreateUser update:
public class UserCommandHandler { @HandleCommand void handle(CreateUser update) { Fluxzero.loadAggregate(update.getUserId()).assertAndApply(update); }}class UserCommandHandler { @HandleCommand fun handle(update: CreateUser) { Fluxzero.loadAggregate(update.userId).assertAndApply(update) }}This loads the UserAccount entity by ID and applies the CreateUser payload after validation.
Entity updates
Section titled “Entity updates”Here’s an example of two commands that update users (modelled as UserAccount) — one to create a user and another to update their profile:
public record CreateUser(UserId userId, UserProfile profile) {
@AssertLegal void assertNotExists(UserAccount current) { throw new IllegalCommandException("Account already exists"); }
@Apply UserAccount apply() { return new UserAccount(userId, profile, false); }}public record UpdateProfile(UserId userId, UserProfile profile) {
@AssertLegal void assertExists(@Nullable UserAccount current) { if (current == null) { throw new IllegalCommandException("Account not found"); } }
@AssertLegal void assertAccountNotClosed(UserAccount current) { if (current.isAccountClosed()) { throw new IllegalCommandException("Account is closed"); } }
@Apply UserAccount apply(UserAccount current) { return current.toBuilder().profile(profile).build(); }}Intercepting and transforming updates
Section titled “Intercepting and transforming updates”Use @InterceptApply to modify or suppress updates before validation and application.
@InterceptApplyObject ignoreNoChange(UserAccount current) { if (current.getProfile().equals(profile)) { return null; // no-op } return this;}@InterceptApplyUpdateProfile downgradeCommand(CreateUser command, UserAccount current) { return new UpdateProfile(command.getUserId(), command.getProfile());}@InterceptApplyList<CreateTask> expandBulk(BulkCreateTasks bulk) { return bulk.getTasks();}Invocation order
Section titled “Invocation order”Update lifecycle steps:
- Intercept using
@InterceptApply - Assert preconditions using
@AssertLegal - Apply state using
@Apply
Interception determines which payloads reach the assertion phase:
| Interceptor outcome | Assertions and application |
|---|---|
| Retain the payload | Its matching immediate @AssertLegal methods run before @Apply |
| Suppress the payload | Neither its assertions nor its apply methods run |
| Replace the payload | Only the replacement’s matching assertions and apply methods run |
| Split the payload | Each part’s immediate assertions and apply run in order; later parts see earlier changes |
An assertion declared only for the original payload is therefore intentionally skipped after suppression or
replacement. Put an invariant that must survive a rewrite on the effective replacement, or in shared or entity-side
assertion logic that also matches it. @AssertLegal(afterHandler = true) keeps its documented deferred timing.
Return types for interceptors:
Section titled “Return types for interceptors:”| Return value | Effect |
|---|---|
null or void | Suppress update |
this | No change |
| New update object | Rewrite the update |
| Collection / Stream / Optional | Emit multiple updates |
Summary
Section titled “Summary”| Annotation | Purpose | Phase |
|---|---|---|
@InterceptApply | Rewrite, suppress, or expand updates | Pre-check |
@AssertLegal | Validate preconditions | Validation |
@Apply | Apply state transformation | Execution |
Why keep logic in the updates?
Section titled “Why keep logic in the updates?”While it’s possible to implement domain logic in entities, it’s usually best to keep validation and transformation logic inside update classes (typically commands).
Advantages of update-based logic:
- Each update owns its behavior
- Entities remain focused on holding state
- Features are easier to isolate and remove
- Tests are simpler and more targeted
Alternative: logic in the entity
Section titled “Alternative: logic in the entity”It’s possible to put validation and logic inside the aggregate itself — but this often leads to bloat:
@Aggregate@Builder(toBuilder = true)public record UserAccount(@EntityId UserId userId, UserProfile profile, boolean accountClosed) {
@AssertLegal static void assertNotExists(CreateUser update, @Nullable UserAccount user) { if (user != null) { throw new IllegalCommandException("Account already exists"); } }
@Apply static UserAccount create(CreateUser update) { return new UserAccount(update.getUserId(), update.getProfile(), false); }
@AssertLegal static void assertExists(UpdateProfile update, @Nullable UserAccount user) { if (user == null) { throw new IllegalCommandException("Account does not exist"); } }
@AssertLegal void assertAccountNotClosed(UpdateProfile update) { if (accountClosed) { throw new IllegalCommandException("Account is closed"); } }
@Apply UserAccount update(UpdateProfile update) { return toBuilder().profile(update.getProfile()).build(); }}Mixing strategies
Section titled “Mixing strategies”Fluxzero supports mixing both styles:
- Use @AssertLegal on the update (command)
- Use @Apply on the entity
- Or vice versa
That said, keeping logic in the update tends to result in simpler, more testable, and easier-to-maintain code.
© 2026 Fluxzero