Nested entities
Fluxzero allows aggregates to contain nested entities — for example, users with authorizations or orders with line
items. These nested entities can be added, updated, or removed using the same @Apply pattern used for root aggregates.
To define a nested structure, annotate the collection or field with @Member:
@Aggregate@Builder(toBuilder = true)public record UserAccount(@EntityId UserId userId, UserProfile profile, boolean accountClosed, @Member List<Authorization> authorizations) {}@Aggregate@Builder(toBuilder = true)data class UserAccount( @EntityId val userId: UserId, val profile: UserProfile, val accountClosed: Boolean, @Member val authorizations: List<Authorization>)Child entities must define their own @EntityId:
public record Authorization(@EntityId AuthorizationId authorizationId, Grant grant) {}data class Authorization( @EntityId val authorizationId: AuthorizationId, val grant: Grant)Adding a child entity
Section titled “Adding a child entity”To add a nested entity like Authorization, simply return a new instance from the @Apply method:
public record AuthorizeUser(AuthorizationId authorizationId, Grant grant) {
@Apply Authorization apply() { return new Authorization(authorizationId, grant); }}data class AuthorizeUser(val authorizationId: AuthorizationId, val grant: Grant) {
@Apply fun apply(): Authorization { return Authorization(authorizationId, grant) }}The UserAccount aggregate is automatically updated to include this new child entity.
Removing a child entity
Section titled “Removing a child entity”To remove a nested entity, return null from the @Apply method:
public record RevokeAuthorization(AuthorizationId authorizationId) {
@AssertLegal void assertExists(@Nullable Authorization authorization) { if (authorization == null) { throw new IllegalCommandException("Authorization not found"); } }
@Apply Authorization apply(Authorization authorization) { return null; }}data class RevokeAuthorization(val authorizationId: AuthorizationId) {
@AssertLegal fun assertExists(authorization: Authorization?) { if (authorization == null) { throw IllegalCommandException("Authorization not found") } }
@Apply fun apply(authorization: Authorization): Authorization? { return null }}Loading entities and aggregates
Section titled “Loading entities and aggregates”Fluxzero supports a flexible and powerful approach to loading aggregates and their internal entities using
Fluxzero.loadAggregateFor(...) and Fluxzero.loadEntity(...).
loadEntity
Section titled “loadEntity”Use this method to load a specific entity without needing to know the aggregate root it belongs to. This allows APIs to remain focused and concise—for example:
public record CompleteTask(TaskId taskId) {}data class CompleteTask(val taskId: TaskId)With Fluxzero, you can handle this using:
Fluxzero.loadEntity(taskId).assertAndApply(new CompleteTask(taskId));Fluxzero.loadEntity(taskId).assertAndApply(CompleteTask(taskId))Even if the Task is deeply nested within a Project or other parent aggregate, this method works because of the
entity relationship tracking automatically maintained by Fluxzero.
Additional behavior:
- If multiple entities match the given ID, the one with the most recently added relationship is used.
- The entire aggregate containing the entity is loaded, ensuring consistency.
- The returned
Entity<T>provides methods likeassertAndApply(...)orapply(...), and includes a reference to the enclosing aggregate root.
loadAggregateFor
Section titled “loadAggregateFor”Use this method to retrieve the aggregate root that currently contains the specified entity ID.
Entity<MyAggregate> aggregate = Fluxzero .loadAggregateFor("some-entity-id");val aggregate: Entity<MyAggregate> = Fluxzero .loadAggregateFor("some-entity-id")Behavior:
- If the ID matches a child entity, the enclosing aggregate is returned.
- If the ID refers to an aggregate root, that root is returned directly.
- If no aggregate exists, an empty aggregate of type
Objectis returned. This enables bootstrapping a new one by applying events.
Finding all aggregates for an entity
Section titled “Finding all aggregates for an entity”In some scenarios, an entity may be referenced by multiple aggregates—for example, when using shared reference
data (e.g. a Currency, Role, or Label). If such an entity is updated, you might want to update all aggregates
that reference it.
To retrieve all aggregates that currently include a given entity ID:
Map<String, Class<?>> aggregates = Fluxzero.get() .aggregateRepository() .getAggregatesFor(myEntityId);val aggregates: Map<String, Class<*>> = Fluxzero.get() .aggregateRepository() .getAggregatesFor(myEntityId)This returns a map of aggregate IDs and their types.
When used responsibly, this enables patterns like:
// Rerender or update every Project referencing a shared Tagfor(Map.Entry<String, Class<?>> entry : Fluxzero.get() .aggregateRepository().getAggregatesFor(tagId).entrySet()) { Fluxzero.loadAggregate(entry.getKey(), entry.getValue()) .apply(new RefreshTag(tagId));}// Rerender or update every Project referencing a shared Tagfor ((id, type) in Fluxzero.get() .aggregateRepository().getAggregatesFor(tagId)) { Fluxzero.loadAggregate(id, type) .apply(RefreshTag(tagId))}Alternative entity identifiers
Section titled “Alternative entity identifiers”Fluxzero supports alternative ways to reference an entity using the @Alias annotation. This is especially useful
when:
- The entity needs to be looked up using a secondary identifier (e.g. an email address or external ID)
- An entity wants to reference another entity without identifier collisions
Lookup via aliases
Section titled “Lookup via aliases”Aliases are used when:
- Loading an aggregate or entity using
Fluxzero.loadAggregateFor(alias)orFluxzero.loadEntity(alias). - Calling
Entity#getEntity(Object id)on a parent entity.
Supported targets
Section titled “Supported targets”You can place @Alias on:
- Fields (e.g.,
@Alias String externalId) - Property methods (e.g.,
@Alias String legacyId())
If the property is a collection, all non-null elements are treated as aliases. If the value is null or an empty
collection, it is ignored.
Prefix and postfix
Section titled “Prefix and postfix”To avoid clashes between IDs in different domains, use the optional prefix and postfix parameters:
@Alias(prefix = "email:")String email;
@Alias(postfix = "@external")String externalId;@Alias(prefix = "email:")val email: String
@Alias(postfix = "@external")val externalId: StringThis ensures that email@example.com is stored as email:email@example.com, and 12345 becomes 12345@external.
Example
Section titled “Example”public record UserAccount(@EntityId String userId, @Alias(prefix = "email:") String emailAddress) {
@Alias List<String> oldIds;}data class UserAccount( @EntityId val userId: String, @Alias(prefix = "email:") val emailAddress: String, @Alias val oldIds: List<String>)Now the UserAccount entity can be looked up using:
Entity<UserAccount> entity = Fluxzero .loadEntity("email:foo@example.com");val entity: Entity<UserAccount> = Fluxzero .loadEntity("email:foo@example.com")or
Entity<UserAccount> entity = Fluxzero .loadEntity("1234"); // one of the oldIdsval entity: Entity<UserAccount> = Fluxzero .loadEntity("1234") // one of the oldIdsStrongly typed @Alias identifiers
Section titled “Strongly typed @Alias identifiers”While @Alias can be applied to any field or property, it’s often more convenient and robust to use it on a
strongly-typed identifier that extends Id<T>:
Id<T>supports prefixing, case-insensitive matching, and type-safe deserialization.- The
@Aliasannotation recognizes the repository ID computed by theId<T>implementation. - You don’t need to repeat the prefix in
@Alias—it’s already encoded in theId.
public class Email extends Id<UserAccount> { public Email(String email) { super(email, "email:"); }}
@AliasEmail email;class Email(email: String) : Id<UserAccount>(email, "email:")
@Aliaslateinit var email: EmailThis allows you to load the entity by its alias:
Entity<UserAccount> account = Fluxzero .loadEntity(new Email("john@example.com"));val account: Entity<UserAccount> = Fluxzero .loadEntity(Email("john@example.com"))This makes aliasing more explicit and reusable — particularly useful in larger applications.
Routing behavior
Section titled “Routing behavior”Flux automatically routes child-targeted updates like AuthorizeUser and RevokeAuthorization to the correct nested
entity using the @EntityId. You don’t need to write custom matching logic — the routing works transparently as long
as:
- The root aggregate is loaded (e.g. using
loadAggregate(userId)), and - The update contains enough identifying information to locate the nested entity
Summary
Section titled “Summary”This model leads to extremely clean domain logic:
- No need to manipulate collections in the aggregate
- No need for boilerplate logic to find, update, or remove children
- Nested updates stay localized to the child entity itself
© 2026 Fluxzero