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. Model references are selected per parameter from the nearest validator declaring them, then enclosing validators, then the triggering payload. Explicit nulls and empty collections do not fall back. Association metadata retains its usual precedence for that parameter, without replacing unrelated selections. Ancestors follow the selected references. This selection does not replace the original payload supplied to payload parameters. 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 when current-input checks are disabled. Assertions then 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. For Models, every ordinary, nested, retained-input and cascading after-handler check sees the complete final state of the atomic Model operation, including later split outputs and automatic child deletions. Each check retains its own payload, metadata and user context. A failure rejects the entire operation before commit; this does not combine separate commands or commits into one transaction.

@InterceptApply(assertCurrent = AssertCurrent.ENABLED) retains checks for that interceptor's input. Immediate checks run before interception within the commit attempt; retained after-handler checks use final state. This applies even to suppressed input. DEFAULT enables this from defaults version 2026.10.04, unless fluxzero.interceptApply.assertCurrent overrides it. Explicit annotation settings win.

Ordering

Cascading rules prevalidate resolved scopes before ordinary assertions; result-dependent routes are checked after the pure apply returns, against the logical before-state. Multiple ordinary legality methods may be invoked. For independently stored models, ordinary 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.
    Class<?>[]
    Restricts independent-Model assertion methods to assignable payload classes/interfaces; empty means unrestricted.
    boolean
    Also validates mutations of known descendants through enabled Parent relationships.
    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
    • cascade

      boolean cascade
      Also validates mutations of known descendants through enabled Parent relationships. The declaring Model's own mutations remain subject to this assertion. Defaults to false. Only Model methods opt into cascading; ordinary aggregate assertions retain their existing semantics. All known enabled routes participate, including old and new routes when reparenting. Shared ancestors run once per effective update/phase. Immediate rules see the logical before-state; after-handler rules see final state. Result-bound routes are checked once the pure apply returns. Normal payload and context injection is retained.

      This is SDK-side validation; unavailable classes and missing parent values stop traversal, and replay does not execute assertions. Deploy the rule to every writer. Read ancestors participate in atomic conflict validation: RETRY reevaluates; configured ACCEPT becomes FAIL for guarded attempts. No new Runtime protocol is required.

      See Also:
      Default:
      false
    • allowedClasses

      Class<?>[] allowedClasses
      Restricts independent-Model assertion methods to assignable payload classes/interfaces; empty means unrestricted.
      Default:
      {}
    • 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