EventListenerExtension
EventListenerExtension lets a plugin react to domain events emitted by the platform — record created/updated/deleted, state transitioned, custom command completed — without coupling to the originating command. Events are dispatched through the platform event bus AuraEventBus to matching listeners; this decouples side effects from the originator, and a single listener failing does not block the others.
Concept
The platform publishes domain events through AuraEventBus.publish(AuraEvent) (or publishAfterCommit(AuraEvent) to dispatch after the transaction commits). PluginEventDispatcher (default implementation DefaultPluginEventDispatcher) pulls matching plugin listeners from ExtensionRegistry and invokes them synchronously, one by one — each listener is isolated, and a thrown exception is caught and logged without interrupting the rest.
A listener declares the event types it subscribes to via getSubscribedEvents(), in "domain:event" form, with wildcard support:
"order:created","order:completed","user:login"— exact match"order:*"— all events in a domain"*:created"— allcreatedevents"*"— all events
ExtensionRegistry.getEventListeners(eventType) filters with isInterestedIn(eventType) and dispatches in ascending getOrder().
Dispatch is synchronous and best-effort: a thrown listener exception is only logged — the framework does not retry, and there is no DLQ. For reliable / transactional side effects, use the
EventPolicytransactional outbox (OutboxWriter) rather than relying on this extension point's delivery semantics. Handlers should still be idempotent.
Interface signature
package com.auraboot.framework.plugin.extension;
import org.pf4j.ExtensionPoint;
import java.util.Map;
import java.util.Set;
public interface EventListenerExtension extends ExtensionPoint {
/** Event types this listener subscribes to, "domain:event" form, with wildcards ("order:*", "*:created", "*"). */
Set<String> getSubscribedEvents();
/** Handle the event. */
void onEvent(EventContext context);
/** Whether interested in the given event type; default matches getSubscribedEvents() wildcards. */
default boolean isInterestedIn(String eventType) { /* wildcard match */ return true; }
/** Execution order, lower runs first. Default 100. */
default int getOrder() { return 100; }
/** Whether async execution is preferred. Default false. */
default boolean isAsync() { return false; }
}EventContext is a record; its fields (same-named accessor methods) are: tenantId(), pluginId(), namespace(), eventType(), sourceModel(), recordId(), eventData() (Map<String, Object>), previousData() (Map<String, Object>), and timestamp() (long).
Implementation example
Subscribe to order-completed events and dispatch a follow-up command:
package com.acme.crm.listener;
import com.auraboot.framework.plugin.extension.EventListenerExtension;
import com.auraboot.framework.meta.service.CommandExecutor;
import com.auraboot.framework.meta.dto.CommandExecuteRequest;
import org.pf4j.Extension;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Map;
import java.util.Set;
@Extension
public class OrderCompletedListener implements EventListenerExtension {
@Autowired private CommandExecutor commandExecutor;
@Autowired private SalesRouter router;
@Override
public Set<String> getSubscribedEvents() {
return Set.of("order:completed");
}
@Override
public void onEvent(EventContext e) {
// sourceModel / recordId / eventData come from the event context
if (!"crm_order".equals(e.sourceModel())) return;
Long rep = router.nextAvailableRep(e.tenantId());
CommandExecuteRequest request = new CommandExecuteRequest();
request.setPayload(Map.of(
"id", e.recordId(),
"assigned_to", rep
));
commandExecutor.execute("crm_order:update", request);
}
}Registration
Annotate with @Extension; the PF4J annotation processor generates META-INF/extensions.idx. No additional configuration is required — the listener is wired into ExtensionRegistry at plugin enable time and unregistered at disable; PluginEventDispatcher pulls from the registry when dispatching.
To publish an event, inject and call AuraEventBus.publish(event) (or publishAfterCommit(event) to dispatch after the transaction commits) from any handler or service.
Common pitfalls
- Dispatch is best-effort, not transactional / reliable delivery. An exception thrown from
onEventis only logged — no retry, no DLQ. For reliable side effects, use theEventPolicytransactional outbox. - Dispatch is synchronous. Your listener runs in the same dispatch loop as the others, in order; slow operations drag down the whole pass. For async work, schedule it yourself inside the listener (
isAsync()is only a preference). - Stay idempotent. The same business event may reach multiple listeners and arrive more than once; dedupe on business keys such as
recordId()/sourceModel(), and never assume exactly-once. - Calling
commandExecutor.executeinside the dispatch context. It runs the full command pipeline; if the event is dispatched inside some transaction, mind the relationship between your write and the originating transaction — usepublishAfterCommitto dispatch after commit when needed.
Related
- Commands — event emission points
- CommandHandlerExtension — in-transaction reactions
- ConversationTurnService — chat-turn event chokepoint