Skip to content

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) {
}

Child entities must define their own @EntityId:

public record Authorization(@EntityId AuthorizationId authorizationId,
Grant grant) {
}

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);
}
}

The UserAccount aggregate is automatically updated to include this new 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;
}
}

Fluxzero supports a flexible and powerful approach to loading aggregates and their internal entities using Fluxzero.loadAggregateFor(...) and Fluxzero.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) {
}

With Fluxzero, you can handle this using:

Fluxzero.loadEntity(taskId).assertAndApply(new 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 like assertAndApply(...) or apply(...), and includes a reference to the enclosing aggregate root.

Use this method to retrieve the aggregate root that currently contains the specified entity ID.

Entity<MyAggregate> aggregate = 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 Object is returned. This enables bootstrapping a new one by applying events.

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);

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 Tag
for(Map.Entry<String, Class<?>> entry : Fluxzero.get()
.aggregateRepository().getAggregatesFor(tagId).entrySet()) {
Fluxzero.loadAggregate(entry.getKey(), entry.getValue())
.apply(new RefreshTag(tagId));
}

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

Aliases are used when:

  • Loading an aggregate or entity using Fluxzero.loadAggregateFor(alias) or Fluxzero.loadEntity(alias).
  • Calling Entity#getEntity(Object id) on a parent entity.

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.

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;

This ensures that email@example.com is stored as email:email@example.com, and 12345 becomes 12345@external.

public record UserAccount(@EntityId String userId,
@Alias(prefix = "email:") String emailAddress) {
@Alias
List<String> oldIds;
}

Now the UserAccount entity can be looked up using:

Entity<UserAccount> entity = Fluxzero
.loadEntity("email:foo@example.com");

or

Entity<UserAccount> entity = Fluxzero
.loadEntity("1234"); // one of the oldIds

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 @Alias annotation recognizes the repository ID computed by the Id<T> implementation.
  • You don’t need to repeat the prefix in @Alias—it’s already encoded in the Id.
public class Email extends Id<UserAccount> {
public Email(String email) {
super(email, "email:");
}
}
@Alias
Email email;

This allows you to load the entity by its alias:

Entity<UserAccount> account = Fluxzero
.loadEntity(new Email("john@example.com"));

This makes aliasing more explicit and reusable — particularly useful in larger applications.

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

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