Decouple Business Logic with Application Events

In CUBA 7 events are the predominant mechanism to react to different kinds of changes of the application. Furthermore, events are a common pattern within an application to decouple one part of the business logic from another. This guide gives an overview on how events can be used in a CUBA application and what the benefits are.

What Will Be Build

This guide enhances the CUBA petclinic example to show different use cases of application events. In particular, the following use cases will be covered:

  • generate a room keycode when a visit is going to be created

  • sending out room keycode for booked visits

  • kicking off payment process once a pet visit is marked as completed

  • refresh the visits list once a visit is marked as completed

Requirements

Your development environment requires to contain the following:

Download and unzip the source repository for this guide, or clone it using git:

Example: CUBA petclinic

The project that is the basis for this example is CUBA Petclinic. It is based on the commonly known Spring Petclinic. The CUBA Petclinic application deals with the domain of a Pet clinic and the associated business workflows to manage a pet clinic.

The underlying domain model for the application looks like this:

Domain model

The main entities are Pet and Visit. A Pet is visiting the petclinic and during this Visit a Vet is taking care of it. A Pet belongs to an Owner, which can hold multiple pets. The visit describes the act of a pet visiting the clinic with the help of its owner.

Benefits of an Event Based Business Logic

The most common approach of communicating across application logic is to do method invocation. In CUBA and Spring this is done via Java objects, Spring components & CUBA services that interact with each other. There are good reasons to use this communication pattern, but it is not the only one. Method invocation is oftentimes overused which can lead to bad maintainability of the overall system due to highly coupled application parts.

Event-based business logic is an alternative communication pattern for application logic, that has particular strengths in its low coupling of the communication partcipants. It is not suitable for all kinds of communication styles, but for a big number of cases it is a viable alternative. Generally event based communication can be used in the following scenarios:

  • notification style communication

  • communication that doesn’t necessarily need a back-channel

  • triggering between technical independent parts of application logic

  • situations where the user doesn’t need an immediate response

As a result of using event based communication the parts of the application that are leveraging it will gain the following properties:

  • low coupling from the event sender to the receiver

  • easy isolated testability of the sender and receiver

  • increased resiliency of the sender

Available Types of Events in a CUBA Application

There are different kinds of application events within a CUBA application. The main categories are:

  • CUBA entity lifecycle events

  • CUBA application lifecycle events

  • CUBA UI events

  • custom application events

Entity Changes Through EntityChangedEvent

In the petclinic example the following logic will be implemented: The petclinic has rooms for the stay of the pets during their time in the clinic. The rooms have no traditional keys or keycards, but instead have a 6 digit keycode to enter at the entrance door. This keycode has to be given to the pet’s owner once a new visit is booked. The transportation of the keycode should happen via SMS notification from the application.

This example is in the category of Entity lifecycle events because we want to execute logic, once a visit is booked (created). The EntityChangedEvent is sent out by the CUBA storage mechanism, once an Entity has been changed in the database.

CUBA fires such an Event for entities, that have an annotation @PublishEntityChangedEvents on the class definition. A EntityChangedEvent contains information about the type of change (create, update or delete) as well as the attributes which have been changed.

To register an Event listener for this event, a new Spring bean in the core module of the application has to be defined. In order to register to a specific kind of event, the method that should be called once an EntityChangedEvent has been sent has to be annotated with @TransactionalEventListener.

There are two different kinds of annotations to register for as an event listener: @TransactionalEventListener and @EventListener. They differ regarding the transaction behavior. In this guide @TransactionalEventListener is used. More details can be found in the CUBA documentation
RoomKeycodeToOwnerSender.java
@Component("petclinic_roomKeycodeToOwnerSender")
public class RoomKeycodeToOwnerSender {

    @Inject
    private DataManager dataManager;

    @Inject
    private MobilePhoneNotificationGateway mobilePhoneNotificationGateway;

    @TransactionalEventListener (1)
    public void sendRoomKeycode(EntityChangedEvent<Visit, UUID> event) { (2)

        if (event.getType().equals(EntityChangedEvent.Type.CREATED)) {
            Visit visit = loadVisit(event.getEntityId()); (3)
            tryToSendRoomKeycodeToPetsOwner(visit);
        }
    }

    private void tryToSendRoomKeycodeToPetsOwner(Visit visit) {

        if (visit.getPet().getOwner() != null) {

            String phoneNumber = visit.getPet().getOwner().getTelephone();

            if (phoneNumber != null) {
                String notificationText = createNotificationText(visit);

                mobilePhoneNotificationGateway.sendNotification(phoneNumber, notificationText);
            }
        }
    }

    // ...

}
1 registration of sendRoomKeycode as an event listener
2 scoping EntityChangedEvent only to Events which changed Visit entities
3 access to Visit identifier of the created instance

With this definition of the Event listener the application will send out room keycodes to the owners of pets that have just registered in the petclinic.

There can be multiple event listeners defined for a given event. In this example it is also required not only to notify the Owner of the pet about the keycode, but also the system that is responsible for controlling the door hardware. It also needs additional information about the visit with the associated pet, to automatically adjust the height of the bed, display a welcome message on the room’s TV etc.

The following event listener will take over the responsibility to notify the room system.

RoomSystemNotifier.java
@Component("petclinic_roomSystemNotifier")
public class RoomSystemNotifier {

    @Inject
    private DataManager dataManager;

    @Inject
    private RoomSystemGateway roomSystemGateway;

    @TransactionalEventListener
    public void notifyRoomSystem(EntityChangedEvent<Visit, UUID> event) {

        if (event.getType().equals(EntityChangedEvent.Type.CREATED)) {
            Visit visit = loadVisit(event.getEntityId());
            tryToNotifyRoomSystemAboutVisit(visit);
        }
    }

    private void tryToNotifyRoomSystemAboutVisit(Visit visit) {
        roomSystemGateway.informAboutVisit(visit);
    }

    // ...
}
When having multiple event listeners the invocation order oftentimes does not matter. In case it is necessary to execute one event listener before another Spring has the ability to define the order via the @Order annotation. See the CUBA documentation for further information on this topic.

Naming Conventions of Events

Events are normally named in simple past: "Entity Changed Event". This is a common pattern, because it stresses that an event is an immutable fact that happened in the past and is not changeable anymore. Event listeners on the other hand should be named in present tense.

Furthermore Event listeners are normally named regarding the one specific action that they should fulfill. Instead of creating a general PetCreatedListener that deals with all things that should happen once a pet is created in the system, Event listeners should be named regarding one specific action that should be performed: RoomKeycodeToOwnerSender → sends a room keycode to the pets owner.

This is an example of an application of the open-closed principle, which normally leads to more loosely coupled application parts and due to that to more maintainable software.

Custom Application Logic Events

The next example is using custom application events to send messages between different parts of the application in a loosely coupled manner. When the pet is recovered and checked out the visit is marked as complete. When this event happens, certain downstream process can be kicked off as well. One of them, which this example deals with is that the invoicing process starts.

The first step towards custom application event is to define an event as a class VisitCompletedEvent in the core module:

VisitCompletedEvent.java
package com.haulmont.sample.petclinic.core.visit;

public class VisitCompletedEvent extends ApplicationEvent { (1)

    private final Visit visit;

    public VisitCompletedEvent(Object source, Visit visit) {

        super(source);
        this.visit = visit;
    }

    public Visit getVisit() {
        return visit;
    }
}
1 Custom events have to extend ApplicationEvent in order to being passed through the event system of Spring

The next part is to send the event using the Events infrastructure of CUBA.

VisitStatusServiceBean.java
package com.haulmont.sample.petclinic.service;

import com.haulmont.cuba.core.global.Events;

// ...

@Service(VisitStatusService.NAME)
public class VisitStatusServiceBean implements VisitStatusService {

    @Inject
    private Events events; (1)

    // ...

    @Override
    public boolean completeVisit(Visit visit) {

        if (visit.getStatus().equals(VisitStatus.ACTIVE)) {
            markVisitAsComplete(visit);

            notifyAboutVisitCompletion(visit);

            return true;
        }
        // ...
    }

    private void notifyAboutVisitCompletion(Visit visit) {
        events.publish(new VisitCompletedEvent(this, visit)); (2)
    }

    private void markVisitAsComplete(Visit visit) {
        visit.setStatus(VisitStatus.COMPLETED);
        dataManager.commit(visit);
        log.info("Visit {} marked as complete", visit);
    }
}
1 Injecting CUBAs Events Spring bean
2 Publishing a new VisitCompletedEvent once the visit’s status is updated

The last remaining part does not differ compared to other events that are published by CUBA itself since the technical mechanisms are exactly the same.

The Event listener InvoicingProcessInitializer receives events of type VisitCompletedEvent and create an invoice for the received visit.

There are two ways to listen to events. Above @TransactionalEventListener annotation is used. There is also a possibility to implement the Interface ApplicationListener<T> which is used for the Event listener InvoicingProcessInitializer.
InvoicingProcessInitializer.java
package com.haulmont.sample.petclinic.core.payment;

@Component("petclinic_invoicingProcessInitializer")
public class InvoicingProcessInitializer implements ApplicationListener<VisitCompletedEvent> {

    // ...

    @Override
    public void onApplicationEvent(VisitCompletedEvent event) {
        log.info("Payment process initialized: {}", event.getVisit());

        CommitContext commitContext = new CommitContext();
        createInvoiceFor(event.getVisit(), commitContext);

        dataManager.commit(commitContext);
    }

    private void createInvoiceFor(Visit visit, CommitContext commitContext) {
        Invoice invoice = dataManager.create(Invoice.class);

        invoice.setVisit(visit);
        invoice.setInvoiceDate(visit.getVisitDate());
        invoice.setInvoiceNumber(createInvoiceNumber());

        List<InvoiceItem> invoiceItems = createInvoiceItemsFor(invoice);
        invoice.setItems(invoiceItems);

        invoiceItems.forEach(commitContext::addInstanceToCommit);
        commitContext.addInstanceToCommit(invoice);

    }

    //...
}

CUBA UI Events

On the UI layer there are two main use cases for events. The first is that the framework itself sends events for certain interactions that happen within the user interface of the application. Additionally, it is also possible just like in the middleware layer to send custom application UI events. Both kinds of events have in common that instead of the middleware events the UI events are always scoped to one instance of the UI. Furthermore, the events are scoped towards a single browser tab.

Framework UI Events

With CUBA 7 the API of the UI components changed from programmatically defined event listener and template method pattern towards declarative event subscriptions via annotations. It is possible to register event listeners in the controller using the @Subscribe annotation.

There are several Events to subscribe that deal with the lifecycle of a controller like InitEvent, BeforeCloseEvent, PreCommitEvent and so on. The data part of the controllers also offer a list of Events like ItemChangeEvent, CollectionChangeEvent. Furthermore, the UI components themselves send events for changes of their states like EnterPressEvent, TextChangeEvent and so on.

CUBA studio has the ability to get an overview of the available events for a controller. It looks like this:

For the petclinic, the VisitEdit controller leverages the InitEntityEvent for generating a Room keycode when a new entity is going to be created:

VisitEdit.java
@UiController("petclinic_Visit.edit")
@UiDescriptor("visit-edit.xml")
@EditedEntityContainer("visitCt")
public class VisitEdit extends StandardEditor<Visit> {

    @Subscribe (1)
    protected void onInitEntity(InitEntityEvent<Visit> event) {
        event.getEntity().setRoomKeycode(generateRoomKeycode()); (2)
    }

    private String generateRoomKeycode() {
        int rookKeycode = new Random().nextInt(999999);
        return String.format("%04d", rookKeycode);
    }
}
1 Registering onInitEntity as an event listener for the InitEntityEvent<Visit>
2 Setting the room keycode of the newly created visit entity

Custom UI Events

Just like for custom application events, custom UI events allow sending notifications about business / UI related facts as objects. The same UI instance is able to register for those events and execute further decoupled logic. In the petclinic, the following use case is modeled as a custom UI event. Once the visit is marked as complete through the UI button, the table of the screen should be updated.

The first step is once again to define a

VisitCompletedUiEvent.java
public class VisitCompletedClickedEvent extends ApplicationEvent implements UiEvent { (1)

    public VisitCompletedClickedEvent(Object source) {
        super(source);
    }
}
1 Marks the application event as a UI event by implementing the interface UiEvent
VisitEdit.java
public class VisitBrowse extends StandardLookup<Visit> {

    // ...

    @Inject
    private Events events; (1)

    @Subscribe("visitsTable.completeVisit")
    protected void completeVisit(Action.ActionPerformedEvent event) {
        Visit visit = visitsTable.getSingleSelected();
        boolean visitWasCompleted = visitStatusService.completeVisit(visit);

        if (visitWasCompleted) {
            events.publish(new VisitCompletedClickedEvent(visit)); (2)
        }
        // ...
    }

    @EventListener (3)
    protected void updateDataOnVisitCompleted(VisitCompletedClickedEvent event) {
        loadData(); (4)

        notifications.create()
                .setCaption(messages.formatMessage(this.getClass(), "visitCompleteSuccessful"))
                .setType(Notifications.NotificationType.TRAY)
                .show();
    }
}
1 Using CUBAs Events mechanism
2 Sending a VisitCompletedUiEvent
3 Registering updateDataOnVisitCompleted as an event listener for VisitCompletedUiEvent events
4 Reload the data

In this example the sender and the receiver are the same class, but this does not necessarily be the case. It is also possible to send a UI event in one controller and receive the event in another controller.

Sending Events Between Layers

Technically there is one main constraint to think about when interacting with events in a CUBA application. This is that as CUBA is a multi-module application that can be deployed as two main parts: frontend (web module) and middleware (core module) sending events between the two layers is not directly possible.

The underlying framework for the events mechanism is Spring which has the facility to send application events within one application, more precisely within one JVM process. Since frontend and middleware can be two different JVM processes on different servers, Spring by default has no capabilities to interact across JVM processes.

There are two ways to solve this problem. The first one is to leverage an external message broker like RabbitMQ to interact between applications (or layers of applications in this case). An internal application event can be send internally. An event listener (e.g. RabbitMqForwarder) then takes this message and forwards it to the external message broker. On the receiver side another Event listener for external RabbitMQ messages converts the messages back to an internal CUBA / Spring messages. With that the application can transparently communicate between JVM boundaries.

The other way is to use an existing application component called global-events. Global events is an application component from Haulmont that especially addresses the problem of communicating between CUBA middleware and frontend parts.

Summary

Application events in CUBA allow defining loosely coupled business logic within the application. Parts independent from a business functionality point of view can also be reflected as independent components within the application. This low coupling has certain advantages like easier testability and higher resiliency of the parts of the application.

But there are also downsides to this approach. Communicating via events can sometimes be a challenge to get a developer’s head around. Method invocation allow a back-channel as a response. This is normally not the case for events. Also identifying that certain parts can be executed independent of each other is sometimes not obvious at first sight.

One could argue that in the example from above the room keycode to the pet’s owner should only be sent if the room system also knows about it and has configured everything properly. This natural wish for consistency is oftentimes very hard to achieve in a distributed system (as the petclinic management and room system are). Messaging in this regard increases the resiliency of the overall system and its parts on the costs of some potential consistency.

Ultimately, modelling and using application events right prevents application logic from ending up in a big ball of mud with invocations between business logic from any parts of the system to any other parts of the system. Sending messages across application parts is a more maintainable way of arranging application logic and oftentimes represents the workflows of the problem domain in a more accurate way as they exist in the real world.