Annotation Interface InterceptApply


@Documented @Retention(RUNTIME) @Target(METHOD) public @interface InterceptApply
Indicates that a method should intercept and potentially transform an update before it is applied to an entity.

This annotation is typically used to:

  • Suppress updates that should be ignored
  • Rewrite or correct invalid updates
  • Split a single update into multiple updates

Unless current-input checks are enabled, interceptors are invoked before any @Apply or @AssertLegal methods. If multiple interceptors match, they are invoked recursively until the result stabilizes. For independently stored models, an interceptor declared on the payload runs before an applicable interceptor declared on the model. The model interceptor receives the payload interceptor's actual output. A replacement with a different payload type starts the same payload-then-model selection for that new type.

Interceptors can return:

Interaction with legality assertions

With current-input checks disabled, interception fully determines the effective update sequence before @AssertLegal and @Apply are considered:
  • A retained update runs its matching immediate assertions before it is applied.
  • A suppressed update runs neither its assertions nor its apply methods.
  • A replacement runs assertions for the replacement, not for the original update.
  • Expanded updates are asserted and applied in encounter order. Each update's immediate assertions see the state produced by earlier updates.
Assertions configured with @AssertLegal(afterHandler = true) remain deferred until handler completion. If an invariant must survive replacement, enable assertCurrent(): the current input's immediate assertions run before interception and its after-handler assertions see final state. Suppressed inputs are checked too. Every replacement keeps its own normal assertions; a bare unchanged input is not checked twice. Model reads participate in atomic conflict checks; RETRY reevaluates and guarded ACCEPT becomes FAIL. DEFAULT follows fluxzero.interceptApply.assertCurrent (FLUXZERO_INTERCEPT_APPLY_ASSERT_CURRENT), enabled from defaults version 2026.10.04. Explicit method overrides take precedence.

Method parameters are automatically injected and may include:

  • The current entity (if it exists)
  • Any parent or ancestor entity in the aggregate
  • Any independently stored @Model loaded for the current model commit, either as its value or as a lazy Graph<T>
  • The update object (if defined on the entity side)
  • Context like Metadata, Message, or User
Injected models are read inputs unless one of the emitted updates later targets and returns them from an @Apply.

Note that empty entities (where the value is null) are not injected unless the parameter is annotated with @Nullable.

Examples

1. Rewrite a duplicate create into an update (inside the update class)

@InterceptApply
UpdateProject resolveDuplicateCreate(Project project) {
    // If this method is invoked, the Project already exists
    return new UpdateProject(projectId, details);
}

2. Suppress a no-op update

@InterceptApply
Object ignoreNoChange(Product product) {
    if (product.getDetails().equals(details)) {
        return null; // suppress update
    }
    return this;
}

Note: You typically do not need to implement this kind of check manually if the enclosing @Aggregate or specific @Apply method is configured with IF_MODIFIED. That configuration ensures that no event is stored or published if the entity is not modified.

3. Expand a bulk update into individual operations

@InterceptApply
List<CreateTask> explodeBulkCreate() {
    return tasks;
}

4. Change graph models in the same commit

@InterceptApply
List<?> move(Graph<Order> order) {
    Graph<OrderLine> line = order.find(lineId, OrderLine.class)
            .orElseThrow();
    return List.of(this, line.update(value -> value.withOrderId(targetOrderId)));
}
The original domain update remains the stored event shared by every changed model. Direct graph updates are replayed against a fresh pinned state after an accepted conflict, so their update function must be deterministic and free of external side effects. Return ordinary domain updates when their @Apply publication configuration should govern the transition; returning a graph produced by apply(...) is deliberately unsupported.

5. Recursive interception

If the result of one @InterceptApply method is a new update object, Fluxzero will look for matching interceptors for the new value as well — continuing recursively until no further changes occur.

See Also:
  • Optional Element Summary

    Optional Elements
    Modifier and Type
    Optional Element
    Description
    Validates the selected interceptor's current input within the same commit attempt, before interception.
  • Element Details

    • assertCurrent

      AssertCurrent assertCurrent
      Validates the selected interceptor's current input within the same commit attempt, before interception. Immediate assertions see the current state; after-handler assertions retain this input and see final state. A suppressed input is still checked. An unchanged input is checked once per scope and phase. Only operations with legality checks enabled use this option; replay and unchecked apply remain unchanged. DEFAULT uses fluxzero.interceptApply.assertCurrent, enabled from defaults version 2026.10.04.
      Default:
      DEFAULT