Events
View MarkdownStatus: Stable Package:
dev.vertique.eventsArtifact:vertique-eventsDepends on:vertique-core,vertx-core,jakarta.inject-apiAPI reference: javadoc.io
vertique-events is the runtime module for compile-time typed event dispatch. It provides an @Observes annotation, the Event<T> abstract publisher, and the ObserverRegistry that matches fired events to registered observers by subtype routing. Observers are registered through Dagger multibinding — no runtime classpath scanning or annotation reflection occurs at dispatch time.
The module provides notification-only semantics: Event<T>.fire(T) always succeeds, and a throwing observer never fails the fan-out or prevents subsequent observers from running.
When To Use It
Section titled “When To Use It”Install this module when application code needs to publish or observe typed events across bean boundaries without coupling publishers to observer lists. Applications inheriting vertique-app-parent receive the complete processor facade automatically; custom-parent applications use the BOM plus vertique-codegen-all recipe in docs/packaging.md. Publisher and registration generation is owned by vertique-codegen-events.
Common use cases:
- Domain event fan-out (e.g.,
OrderCreatednotifying audit, notification, and stock modules) - Lifecycle hooks across independent beans without shared interfaces
- Decoupling framework modules from application concerns
Core Concepts
Section titled “Core Concepts”Event type inventory. The processor (vertique-codegen-events) builds a type inventory from @Observes parameter types and constructor-injected Event<T> injection points. For each type in the inventory, a concrete X$Event extends Event<X> publisher is generated and bound so that Event<X> is injectable.
Subtype routing. ObserverRegistry.match(Class<?>) returns all observers whose declared event type is the fired type, a supertype of it (walking the superclass chain), or an interface transitively implemented by it. An observer declared for AnimalCreated automatically receives a fired DogCreated event if DogCreated is a subtype of AnimalCreated.
Notification-only contract. Event<T>.fire(T) returns Future<Void>. The future completes successfully after all matched observers have settled. Any Throwable — including Error subclasses — thrown by an observer is swallowed and logged (event type name and throwable class only; no payload is logged). The publisher’s future never fails.
Sequential, priority-ordered dispatch. Observers are dispatched in ascending priority order (lower fires earlier), sequentially, using Combinators.foldSequential. The ordering of observers with equal priority is unspecified and depends on the iteration order of the injected Set<ObserverRegistration>; callers that need a strict ordering must assign distinct priority values.
Observer constraints. Observers are void-returning and synchronous (void-returning methods only). Async observer support (Future<Void>-returning) is deferred.
Key Classes
Section titled “Key Classes”@Observes
Section titled “@Observes”Parameter annotation that marks a method as an observer of its parameter’s type.
@Target(ElementType.PARAMETER)@Retention(RetentionPolicy.SOURCE)public @interface Observes { int priority() default 1000;}@Observes is SOURCE-retained — it is invisible at runtime and across module boundaries. Cross-module aggregation of observers is achieved through Dagger @IntoSet ObserverRegistration multibinding generated by vertique-codegen-events, not through runtime annotation scanning.
Usage:
@Singletonpublic class OrderAuditListener {
@Inject public OrderAuditListener() {}
public void onOrderCreated(@Observes OrderCreated event) { // synchronous, void-returning observer auditLog.record(event.orderId()); }
public void onOrderCreated(@Observes(priority = 500) OrderCreated event) { // fires BEFORE the default priority (1000) observer above }}Observer methods must:
- return
void - declare exactly one parameter
- annotate that parameter with
@Observes - not observe
java.lang.Objectdirectly (the registry’s superclass walk excludesObject)
Invariants & Gotchas
Section titled “Invariants & Gotchas”- Equal-priority observers have unspecified relative order. Assign distinct
priorityvalues when sequence matters. @Observes Objectis rejected at compile time: the registry excludesObjectfrom the supertype walk, so such an observer would silently never fire.
Event<T>
Section titled “Event<T>”Abstract publisher class. Application code injects the parameterized type Event<X> and calls fire(X). The concrete X$Event extends Event<X> subclass is generated by vertique-codegen-events.
public abstract class Event<T> { public Future<Void> fire(T event) { ... }}Usage:
@Singletonpublic class OrderService {
private final Event<OrderCreated> orderCreated;
@Inject public OrderService(Event<OrderCreated> orderCreated) { this.orderCreated = orderCreated; }
public Future<OrderId> createOrder(CreateOrderRequest req) { return persist(req) .compose(order -> orderCreated.fire(new OrderCreated(order.id())) .map(ignored -> order.id())); }}fire() dispatches to every observer whose declared event type is OrderCreated or a supertype of OrderCreated (including interfaces). The returned future completes successfully once all observers have settled.
Injecting Event<X> when no observers are registered for X (and no @Observes X is declared anywhere) requires including the event type as an Event<X> injection point in an @Inject constructor — this places X in the inventory so the processor generates a no-op publisher. An unregistered event type fires successfully as a no-op.
ObserverRegistry
Section titled “ObserverRegistry”Singleton registry that indexes the application’s observer registrations and matches them against a fired event type.
@Singletonpublic final class ObserverRegistry { @Inject public ObserverRegistry(Set<ObserverRegistration> registrations) { ... }
public List<ObserverRegistration> match(Class<?> firedType) { ... }}Built at startup from the Dagger-aggregated Set<ObserverRegistration>. At dispatch time, match(firedType) builds the full supertype set of firedType (the type itself, every superclass up to but not including Object, and all transitively reachable interfaces via TypeResolver.getAllInterfaces), filters the registered observers against that set, removes duplicates, and returns them sorted by ascending priority.
Invariants & Gotchas
Section titled “Invariants & Gotchas”Object.classis excluded from the supertype walk. An observer that declares@Observes Objectis rejected at compile time.match()is called on everyfire()invocation and performs aSetfilter + sort. Registry instances are@Singleton; theSet<ObserverRegistration>is fixed at startup.
ObserverRegistration
Section titled “ObserverRegistration”Immutable record carrying the runtime descriptor of a single observer method.
public record ObserverRegistration( Class<?> eventType, int priority, Consumer<Object> invoke) {}invoke is a direct, reflection-free lambda generated by vertique-codegen-events — no Method.invoke. The lambda casts the event object to the declared event type and calls the observer method on the Dagger-injected bean instance. Type safety is enforced by the processor at generation time; a runtime type mismatch would be a processor bug, not a user error.
Extension Points
Section titled “Extension Points”ObserverRegistration (via @Provides @IntoSet)
Section titled “ObserverRegistration (via @Provides @IntoSet)”Observers participate in the application’s event fan-out by contributing an ObserverRegistration to the Set<ObserverRegistration> multibinding. This happens automatically through the GeneratedEventsModule generated by vertique-codegen-events. Manual registration is possible for cases where the processor is not used:
@Provides @IntoSetstatic ObserverRegistration myObserver(MyBean bean) { return new ObserverRegistration( OrderCreated.class, 1000, event -> bean.onOrderCreated((OrderCreated) event) );}Dependencies
Section titled “Dependencies”| Artifact | Purpose |
|---|---|
vertique-core |
Combinators.foldSequential (sequential fan-out with swallow step); TypeResolver.getAllInterfaces (interface supertype walk in ObserverRegistry) |
vertx-core |
io.vertx.core.Future — the return type of Event.fire() |
jakarta.inject-api |
@Inject, @Singleton on ObserverRegistry |