CommandHandlerExtension
CommandHandlerExtension is the primary extension point for injecting custom Java logic into AuraBoot's Command Pipeline. Every write operation in the platform — create, update, delete, state transitions, custom commands — flows through CommandExecutor, which dispatches across 16 pipeline phases (Load → Command Authorization → Schema Validate → Idempotency → Entitlement → SoD Check → State Check → Assert → Pre-Actions → Pre-Invariant → Auto Set → Field Map → Computed Fields → Handler → Post-Execution → Completion). A CommandHandlerExtension binds to a specific command type and contributes logic at the Handler phase.
Concept
A handler is scoped to a single command type, matched by the namespace:command-name value returned from getCommandType(). The pipeline invokes execute(CommandContext) at the Handler phase, which returns a result object (may be null).
The platform runs exactly one primary handler per command — the highest-getPriority() handler among those whose chainsAfterPrimary() returns false (higher priority wins). A handler that returns true from chainsAfterPrimary() is a secondary: it does not participate in primary selection and instead runs after the primary, inside the same command transaction, letting a downstream plugin add behaviour to a command another plugin already owns. Multiple secondaries run in descending getPriority() order.
All write side effects inside execute participate in the surrounding command transaction owned by CommandExecutor. Do not swallow exceptions — see Common pitfalls.
Interface signature
package com.auraboot.framework.plugin.extension;
import org.pf4j.ExtensionPoint;
import java.util.Set;
public interface CommandHandlerExtension extends ExtensionPoint {
/** Command type this handler processes. Format "namespace:command-name", e.g. "billing:generate-invoice". */
String getCommandType();
/** All command types this handler can process (import-time validation needs a finite list). Default: {getCommandType()}. */
default Set<String> getSupportedCommandTypes() {
String commandType = getCommandType();
return commandType == null ? Set.of() : Set.of(commandType);
}
/** Execute the command. Returns a result object (may be null). */
Object execute(CommandContext context) throws Exception;
/** Whether this handler supports the given command type; default exact match on getCommandType(). */
default boolean supports(String commandType) {
return getCommandType().equals(commandType);
}
/** Handler priority; higher runs first. Default 0. */
default int getPriority() {
return 0;
}
/** Run as a secondary chained after the primary; default false (participates in primary selection). */
default boolean chainsAfterPrimary() {
return false;
}
/** Whether this handler is safe under dryRun; default false (skipped under dry-run). */
default boolean supportsDryRun() {
return false;
}
}CommandContext is a record; fields are read through record accessors: ctx.tenantId(), ctx.commandType(), ctx.modelCode(), ctx.recordId(), ctx.payload() (Map<String, Object>), ctx.settings() (Map<String, Object>), ctx.pluginId(), ctx.namespace(), ctx.dryRun(). Data access goes through ctx.dataAccessor() (or ctx.settings().get("__dataAccessor")) to reach the platform-injected DataAccessor; convenience bridges include ctx.aiProviderAccessor(), ctx.fileAccessor(), and ctx.asyncTaskAccessor().
Implementation example
The following handler processes the crm:create_lead command, computes a score, and writes it back to the payload:
package com.acme.crm.handler;
import com.auraboot.framework.plugin.extension.CommandHandlerExtension;
import com.auraboot.framework.plugin.extension.DataAccessor;
import org.pf4j.Extension;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Map;
@Extension
public class LeadScoringHandler implements CommandHandlerExtension {
@Autowired private LeadScoringService scoring;
@Override
public String getCommandType() {
return "crm:create_lead";
}
@Override
public Object execute(CommandContext ctx) throws Exception {
DataAccessor db = ctx.dataAccessor();
Map<String, Object> payload = ctx.payload();
Integer score = scoring.score(payload);
payload.put("lead_score", score);
return Map.of("recordId", ctx.recordId(), "score", score);
}
}Registration
AuraBoot uses PF4J-style extension discovery (@Extension) layered on Spring. Two steps:
- Annotate the class with
@Extension(fromorg.pf4j). - List the extension class in
META-INF/extensions.idx, generated at compile time by theorg.pf4j:pf4jannotation processor (annotationProcessor 'org.pf4j:pf4j').
In plugin.json, ensure the extension's package is included in plugin class scanning. Handler-to-command binding is by interface + annotation + getCommandType() (runtime dispatch then goes through supports(...)), not by a handler reference in commands.json or bindingRules.json.
To call other services, use standard Spring @Autowired. The plugin classloader resolves platform beans through the host's ApplicationContext.
Common pitfalls
- Swallowing exceptions. A
catch (Exception e) { log.warn(...) }insideexecutewhile the pipeline holds an active transaction results inrollback-onlyflags but no surfaced error — see red line §8. Re-throw, or use@Transactional(NOT_SUPPORTED)on out-of-band side effects. - Inline
bindingRulesincommands.json. Not loaded by the importer — see red line §6. Handler-to-command binding is purely bygetCommandType(). - External side effects under dry-run. Transaction rollback only undoes JDBC writes; HTTP / email / MQ / object-storage side effects escape the rollback envelope.
supportsDryRun()defaults tofalse, so the handler is skipped under dry-run; before returningtrueyou must inspectctx.dryRun()and short-circuit every external call. - Long-running I/O in
execute. You are holding the request thread plus the transaction. Publish to the outbox or process asynchronously viactx.asyncTaskAccessor().
Related
- Commands — pipeline phase reference
- CommandExecutor service
- ValidatorExtension — for field-level validation
- EventListenerExtension — for event-driven async reactions