EventBusService (interface)

A service implementing an Event Bus, allowing domain objects to emit arbitrary events on an in-memory event bus.

Events are delivered synchronously to event subscribers (domain services).

API

EventBusService.java
interface EventBusService {
  void post(Object event)     (1)
}
1 post(Object)

Post an event (of any class) to the in-memory event bus.

Members

post(Object)

Post an event (of any class) to the in-memory event bus.

The event will be delivered synchronously (within the same transactional boundary) to all subscribers of that event type (with subscribers as domain services with public method annotated using Spring’s org.springframework.context.event.EventListener annotation.

Implementation

The framework provides a default implementation of the service, o.a.i.core.runtimeservices.eventbus.EventBusServiceSpring.

Usage by the Framework

The primary user of the service is the framework itself, which automatically emit events for actions, properties and collections. Multiple events are generated:

  • when an object member is to be viewed, an event is fired; subscribers can veto (meaning that the member is hidden)

  • when an object member is to be enabled, the same event instance is fired; subscribers can veto (meaning that the member is disabled, ie cannot be edited/invoked)

  • when an object member is being validated, then a new event instance is fired; subscribers can veto (meaning that the candidate values/action arguments are rejected)

  • when an object member is about to be changed, then the same event instance is fired; subscribers can perform pre-execution operations

  • when an object member has been changed, then the same event instance is fired; subscribers can perform post-execution operations

If a subscriber throws an exception in the first three steps, then the interaction is vetoed. If a subscriber throws an exception in the last two steps, then the transaction is aborted. For more on this topic, see @Action#domainEvent(), @Property#domainEvent() and @Collection#domainEvent().

It is also possible for domain objects to programmatically generate domain events. However the events are published, the primary use case is to decoupling interactions from one module/package/namespace and another.

Default Event Classes

The framework will automatically emit domain events for all of the object members (actions, properties or collections) of an object whenever that object is rendered or (more generally) interacted with.

For example:

public class Customer {
    @Action()
    public Customer placeOrder( Product product, int quantity) {
        ...
    }
    ...
}

will propagate an instance of the default o.a.i.applib.services.eventbus.ActionDomainEvent.Default class, which can then be subscribed to:

import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;

@Service
public class MySubscribingDomainService {
    @EventListener(ActionDomainEvent.class)
    public void on(ActionDomainEvent ev) {
        ...
    }
    ...
}

Custom Event Classes

More commonly though you will probably want to emit domain events of a specific subtype. As a slightly more interesting example, suppose in a library domain that a LibraryMember wants to leave the library. A letter should be sent out detailing any books that they still have out on loan:

In the LibraryMember class, we publish the event by way of an annotation:

public class LibraryMember {
    @Action(domainEvent=LibraryMemberLeaveEvent.class)  (1)
    public void leave() {
        ...
    }
    ...
}
1 LibraryMemberLeaveEvent is a subclass of o.a.i.applib.eventbus.ActionDomainEvent.The topic of subclassing is discussed in more detail below.

Meanwhile, in the BookRepository domain service, we subscribe to the event and act upon it. For example:

import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;

@Service
public class BookRepository {

    @EventListener(LibraryMemberLeaveEvent.class)
    public void onLibraryMemberLeaving(LibraryMemberLeaveEvent e) {
        LibraryMember lm = e.getLibraryMember();
        List<Book> lentBooks = findBooksOnLoanFor(lm);
        if(!lentBooks.isEmpty()) {
            sendLetter(lm, lentBooks);
        }
    }

}

This design allows the libraryMember module to be decoupled from the book module.

Event hierarchy

By creating domain event subtypes we can be more semantically precise and in turn providesmore flexibility for subscribers: they can choose whether to be broadly applicable (by subscribing to a superclass) or to be tightly focussed (by subscribing to a subclass).

We recommend that you define event classes at (up to) four scopes:

  • at the top "global" scope is the Apache Isis-defined o.a.i.applib.event.ActionDomainEvent

  • for the "module" scope, create a static class to represent the module itself, and creating nested classes within

  • for each "class" scope, create a nested static event class in the domain object’s class for all of the domain object’s actions

  • for each "action" scope, create a nested static event class for that action, inheriting from the "domain object" class.

To put all that into code; at the module level we can define:

package com.mycompany.modules.libmem;
...
public static class LibMemModule {
    private LibMemModule() {}
    public abstract static class ActionDomainEvent<S>
                extends org.apache.isis.applib.event.ActionDomainEvent<S> {}
    ...                                                                             (1)
    public abstract static class PropertyDomainEvent<S,T>
                extends org.apache.isis.applib.event.PropertyDomainEvent<S,T> {}
    public abstract static class CollectionDomainEvent<S,E>
                extends org.apache.isis.applib.event.CollectionDomainEvent<S,E> {}
}
1 similar events for properties and collections should also be defined

For the class-level we can define:

public static class LibraryMember {
    public abstract static class ActionDomainEvent
            extends LibMemModule.ActionDomainEvent<LibraryMember> { }
    ...                                                                             (1)
}
1 similar events for properties and collections should also be defined

and finally at the action level we can define:

public class LibraryMember {
    public static class LeaveEvent extends LibraryMember.ActionDomainEvent { }
    @Action(domainEvent=LeaveEvent.class)
    public void leave() {
        //...
    }
    ...
}

The subscriber can subscribe either to the general superclass (as before), or to any of the classes in the hierarchy.

Variation (for contributing services)

A slight variation on this is to not fix the generic parameter at the class level, ie:

public static class LibraryMember {
    public abstract static class ActionDomainEvent<S>
            extends LibMemModule.ActionDomainEvent<S> { }
    ...
}

and instead parameterize down at the action level:

public class LibraryMember {
    public static class LeaveEvent
            extends LibraryMember.ActionDomainEvent<LibraryMember> { }

    @Action(domainEvent=LeaveEvent.class)
    public void leave() {
        ...
    }

    ...
}

This then allows for other classes - in particular domain services contributing members - to also inherit from the class-level domain events.

Programmatic posting

To programmatically post an event, simply call EventBusService#post.

The LibraryMember example described above could for example be rewritten into:

public class LibraryMember {
    @Action()
    public void leave() {
        ...
        eventBusService.post(new LibraryMember.LeaveEvent(/*...*/));    (1)
    }
    ...
}
1 LibraryMember.LeaveEvent could be any class, not just a subclass of o.a.i.applib.event.ActionDomainEvent.

In practice we suspect there will be few cases where the programmatic approach is required rather than the declarative approach afforded by @Action#domainEvent() et al.

Using WrapperFactory

Using the declarative approach does require that the method to emit the event is an action called directly by the framework (rather than a helper method programmatically called by that action). However by using the WrapperFactory we can invoke that helper method "through" the framework, thereby allowing the framework to emit events. (It can also optionally perform validation checks and other concerns associated with the UI).

Another use case for the WrapperFactory is when you wish to enforce a (lack-of-) trust boundary between the caller and the callee.

For example, suppose that Customer#placeOrder(…​) emits a PlaceOrderEvent, which is subscribed to by a ReserveStockSubscriber. This subscriber in turn calls StockManagementService#reserveStock(…​). Any business rules on #reserveStock(…​) should be enforced.

In the ReserveStockSubscriber, we therefore use the WrapperFactory:

import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;

@Service
public class ReserveStockSubscriber {

    @EventListener(Customer.PlaceOrderEvent.class)
    public void on(Customer.PlaceOrderEvent ev) {
        wrapperFactory.wrap(stockManagementService)
                      .reserveStock(ev.getProduct(), ev.getQuantity());
    }

    @Inject
    StockManagementService stockManagementService;
    @Inject
    WrapperFactory wrapperFactory;
}

The EventBusService is intended for fine-grained publish/subscribe for object-to-object interactions within an Apache Isis domain object model. The event propagation is strictly in-memory, and there are no restrictions on the object acting as the event (it need not be serializable, for example).

There are several mechanisms to suport coarse-grained publish/subscribe for system-to-system interactions, from Apache Isis to some other system: