Annotation Interface AssertLegal


@Target({METHOD,FIELD,ANNOTATION_TYPE}) @Retention(RUNTIME) public @interface AssertLegal
Annotation to mark methods or fields that assert whether a command or query is legal, given the current state of an aggregate.

Can be used in two ways:

  • On methods inside a command or query class to perform legality checks directly
  • On properties of a command or query class to delegate legality checks to the annotated property’s class

Method-based usage

Annotated methods are invoked after the aggregate and its entities are loaded using Entity.assertLegal(Object) or Entity.assertAndApply(Object).

Parameters are injected automatically and may include:

  • The command or query object itself
  • Any matching entity from the aggregate tree (including parent or grandparent entities)
  • Any independently stored @Model loaded for the current model commit, either as its value or as Entity<T>
  • Other framework-specific types like Message or User
Commit-scoped models injected into an assertion are read-only inputs; injection alone does not target or store them.

Note that empty entities (i.e., those with null values) are not injected unless the parameter is annotated with @Nullable.

Example: Validate entity does not exist yet

@AssertLegal
void assertNew(Issue issue) {
    throw new IllegalCommandException("Issue already exists");
}

Example: Validate entity does exist

@AssertLegal
void assertExists(@Nullable Issue issue) {
    if (issue == null) {
        throw new IllegalCommandException("Issue not found");
    }
}

Property-based usage

When placed on a field of a command or query payload (e.g., @AssertLegal UserDetails details), the framework will look for @AssertLegal methods or fields within that field’s value.

This enables modular legality checks colocated with the data they validate.

Example

public class UpdateUser {
    UserId userId;

    @AssertLegal
    UserDetails details; // triggers @AssertLegal methods inside UserDetails
}

Return value inspection

If an @AssertLegal method returns a non-null object, Fluxzero will also inspect that return value for further @AssertLegal methods or properties. This allows for deep, composable validation logic.

For independent Models, return values (including collection elements) are traversed in the returning method's phase. Nested methods keep the original payload, metadata, user and application parameter resolvers; injected Models are loaded at the same pinned read boundary and participate in conflict validation. Null values are ignored. Each object identity is visited once within a payload or Model assertion phase, so shared references and cycles do not execute its checks repeatedly. Nesting beyond 256 levels is rejected.

Annotated Model fields, including record components, delegate in both phases; methods on their values select their own afterHandler() timing. A method is invoked only in its declared phase, even when it is a no-arg accessor. To share one validation object between before and after checks, expose it through an annotated field rather than relying on a before-method being invoked again after apply. Fluxzero.assertLegal(Object) performs only immediate checks. Rebase under ACCEPT and event replay do not rerun assertions.

Interaction with intercepted updates

@InterceptApply resolves the effective update or updates first. Assertions therefore run for a retained update, do not run for a suppressed update, and run only for the replacement when the original update is replaced. Expanded updates are processed in encounter order: each update's immediate assertions run before its apply methods and see state produced by earlier updates. Assertions configured with afterHandler() remain deferred until handler completion.

If a rule must also hold after an interceptor replaces the original payload, define that rule for the effective replacement or place it in shared/entity-side assertion logic that matches the replacement. An assertion that only matches the original payload is intentionally not invoked after replacement.

Ordering

Multiple legality methods may be invoked. For independently stored models, assertions declared on the payload run before assertions declared on the model. Within each phase their execution order is determined by priority(), with higher values taking precedence. Methods with the same priority are invoked in deterministic order by method name and signature. Immediate assertions see the state before apply; deferred assertions use the same payload-then-model order against the fully composed state.

Execution timing

By default, checks run immediately during handler execution. You can defer them until after the handler completes (after any @Apply invocations but just before aggregate updates are committed) using afterHandler().
See Also:
  • Field Summary

    Fields
    Modifier and Type
    Field
    Description
    static final int
     
    static final int
     
    static final int
     
  • Optional Element Summary

    Optional Elements
    Modifier and Type
    Optional Element
    Description
    boolean
    Determines if the legality check should be performed immediately (the default), or when the current handler is done, i.e.: after @Apply and just before the aggregate updates are committed.
    int
    Determines the order of assertions if there are multiple annotated methods (or fields on independent Models).
  • Field Details

  • Element Details

    • priority

      int priority
      Determines the order of assertions if there are multiple annotated methods (or fields on independent Models). A method with higher priority will be invoked before methods with a lower priority. Use HIGHEST_PRIORITY to ensure that the check is performed first. Methods with the same priority are invoked in deterministic order by method name and signature.
      Default:
      0
    • afterHandler

      boolean afterHandler
      Determines if the legality check should be performed immediately (the default), or when the current handler is done, i.e.: after @Apply and just before the aggregate updates are committed. For independent Models, this selects method timing; annotated fields delegate to their values in both phases.
      Default:
      false