I recently developed a plugin for SpringWolf that configures an Eventuate service to expose an /springwolf/docs
endpoint that returns an Async API document describing the messages that the service sends and receives.
While implementing the plugin, however, I discovered that the existing Eventuate APIs were not well suited to exposing Async API-style metadata.
For example, the messages sent by the service — and the channels to which it sent them — were not explicitly defined; instead, these details were hidden within the code
This realization prompted the creation of a new set of Eventuate APIs for sending and receiving messages. The old APIs still work but only limited metadata can be generated from them. In this article, I’ll describe the new APIs for publishing and subscribing to events. A later article will describe the new APIs for commands and sagas. Let’s first look at the changes to event publishing.
Let’s first look at the old way of publishing events. After that I describe the new approach.
Previously, domain logic simply used the DomainEventPublisher
to publish events:
public class CustomerService {
private DomainEventPublisher domainEventPublisher;
@Transactional
public Customer createCustomer(String name, Money creditLimit) {
...
domainEventPublisher.publish(Customer.class, customer.getId(), customerWithEvents.events);
return customer;
}
As a result, the channels and event types were not explicitly defined.
The new approach consists of defining one or more DomainEventPublisherForAggregate
@Beans
.
A DomainEventPublisherForAggregate
publishes events for a specific aggregate.
Although defining DomainEventPublisherForAggregates
involves more code, it explicitly defines the channels and events.
The Eventuate plugin for Spring Wolf uses the DomainEventPublisherForAggregate
@Beans
to generate an Async API document describing the published events and their channels.
Here’s the definition of a DomainEventPublisherForAggregate
for the Customer
aggregate:
public interface CustomerEventPublisher extends DomainEventPublisherForAggregate<Customer, Long, CustomerEvent> {
}
It’s injected into the CustomerService
:
public class CustomerService {
private final CustomerEventPublisher customerEventPublisher;
public void reserveCredit(long orderId, long customerId, Money orderTotal) {
CustomerCreditReservedEvent customerCreditReservedEvent =
new CustomerCreditReservedEvent(customerId, orderId);
customerEventPublisher.publish(customer, customerCreditReservedEvent);
Finally, here is the @Bean
implementation of the CustomerEventPublisher
:
@Component
public class CustomerEventPublisherImpl extends AbstractDomainEventPublisherForAggregateImpl<Customer, Long, CustomerEvent> implements CustomerEventPublisher {
public CustomerEventPublisherImpl(DomainEventPublisher domainEventPublisher) {
super(Customer.class, Customer::getId, domainEventPublisher, CustomerEvent.class);
}
}
Let’s now look at the new event handling API.
Let’s first look at the old way of subscribing to events. After that I describe the new approach.
Previously, event handlers were configured by defining a class that constructed DomainEventHandlers
:
public class OrderEventConsumer {
public DomainEventHandlers domainEventHandlers() {
return DomainEventHandlersBuilder
.forAggregateType("io.eventuate.examples.tram.ordersandcustomers.orders.domain.Order")
.onEvent(OrderCreatedEvent.class, this::handleOrderCreatedEvent)
...
.build();
}
public void handleOrderCreatedEvent(DomainEventEnvelope<OrderCreatedEvent> domainEventEnvelope) {
...
The DomainEventHandlers
was then passed to a DomainEventDispatcherFactory
:
public class CustomerConfiguration {
@Bean
public OrderEventConsumer orderEventConsumer(CustomerService customerService) {
return new OrderEventConsumer(customerService);
}
@Bean
public DomainEventDispatcher domainEventDispatcher(OrderEventConsumer orderEventConsumer, DomainEventDispatcherFactory domainEventDispatcherFactory) {
return domainEventDispatcherFactory.make("orderServiceEvents", orderEventConsumer.domainEventHandlers());
}
The Eventuate plugin for Spring Wolf can use the DomainEventDispatcher
@Beans
to generate an Async API document describing the subscribed to events and their channels.
However, there’s no obvious way to customize the Async API document by, for example, specifying additional documentation.
The new approach is to define beans that have methods annotated with @EventuateDomainEventHandler
.
The annotation specifies the event handler’s subscriber ID and the channel.
The event type is obtained from the method parameter.
@Component
public class OrderEventConsumer {
private Logger logger = LoggerFactory.getLogger(getClass());
private CustomerService customerService;
public OrderEventConsumer(CustomerService customerService) {
this.customerService = customerService;
}
@EventuateDomainEventHandler(subscriberId = "OrderEventConsumer", channel = "io.eventuate.examples.tram.ordersandcustomers.orders.domain.Order")
public void handleOrderCreatedEvent(DomainEventEnvelope<OrderCreatedEvent> domainEventEnvelope) {
OrderCreatedEvent event = domainEventEnvelope.getEvent();
customerService.reserveCredit(Long.parseLong(domainEventEnvelope.getAggregateId()),
event.orderDetails().customerId(), event.orderDetails().orderTotal());
}
The Eventuate plugin for Spring Wolf can search the application context for @Beans
that have methods annotated with @EventuateDomainEventHandler
.
Moreover, in the future, it will support additional annotations that allow you to customize the generated Async API document.
eventuate-tram-examples-customers-and-orders
repository contains an example application that uses the new APIs.A later post will describe the new APIs for sagas and command handlers.