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" — all created events
  • "*" — 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 EventPolicy transactional 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 onEvent is only logged — no retry, no DLQ. For reliable side effects, use the EventPolicy transactional 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.execute inside 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 — use publishAfterCommit to dispatch after commit when needed.

Related