Domain Driven Design Tactical Patterns--Domain Events

Keywords: Java Spring REST Attribute Database

Use domain events to capture something that happens in the domain.

Domain-driven practitioners find that they can better understand the problem domain by understanding more events that occur in the problem domain. These events are domain events, which are mainly acquired in the process of knowledge extraction with domain experts.

Domain events can be used in a domain model within a bounded context or asynchronous communication between bounded contexts using message queues.

1 Understanding Domain Events

Domain events are some of the events that occur in the domain that domain experts are concerned about.

Modeling activities occurring in the domain as a series of discrete events. Each event is represented by a domain object. Domain events are part of the domain model, representing what happens in the domain.

Main uses of domain events:

  • Ensuring data consistency between aggregates
  • Replacement batch processing
  • Implementing Event Source Patterns
  • Boundary Context Integration

2. Implementing domain events

Domain events represent the fact that something has happened, and that fact will not change after it happens. Therefore, domain events are usually modeled as value objects.

However, there are special situations in which modelling is often compromised to meet the needs of serialization and deserialization frameworks.

2.1 Create Domain Events

2.1.1 Event Naming

When modeling domain events, we should name events according to the common language in the context of boundaries.

If an event is generated by a command operation on an aggregation, it is usually named after the name of the operation method. The event name indicates the fact that the command method on the aggregation has been successfully executed. Naming events needs to reflect what happened in the past.

public class AccountEnabledEvent extends AbstractAggregateEvent<Long, Account> {
    public AccountEnabledEvent(Account source) {
        super(source);
    }
}
2.1.2 Event Properties

The attributes of events are mainly used to drive subsequent business processes. Of course, there are some general attributes.

Events have some common attributes, such as:

  • Unique identification
  • Occurrence time of occurrenceredOn
  • Type event type
  • Source event occurrence source (only for events generated by aggregation)

Common attributes can be standardized using event interfaces.

Interface or class Meaning
DomainEvent Universal Domain Event Interface
AggregateEvent General Domain Event Interface Published by Aggregation
AbstractDomainEvent DomainEvent implementation class, maintenance id and creation time
AbstractAggregateEvent AggregateEvent implements the class, inherits the child AbstractDomainEvent, and adds the source attribute

However, business attributes are the most important attributes of events. We need to consider who caused the event, which may involve the aggregation of events or other aggregation involved in the operation, or any other type of operation data.

2.1.3 Event Method

Events are factual descriptions, and there won't be too many business operations per se.

Domain events are usually designed as immutable objects, and the data carried by the event reflects the source of the event. Event constructors complete state initialization and provide getter methods for attributes.

2.1.4 Event Unique Identification

What we need to pay attention to here is the unique identification of events. Usually, events are immutable. Why does it involve the concept of unique identification?

For domain events published from aggregation, the use of event name, event identification, event occurrence time and so on is sufficient to distinguish different events. However, this will increase the complexity of event comparison.

For events published by callers, we model domain events as aggregates, using the unique identifier of aggregation directly as the identifier of events.

The introduction of event unique identifiers greatly reduces the complexity of event comparisons. However, its greatest significance lies in the integration of bounded contexts.

When we need to publish domain events to external bounding contexts, the only identification is a necessity. In order to ensure the idempotency of event delivery, at the sending end, we may attempt to send multiple times until it is clear that the transmission is successful; at the receiving end, when the event is received, we need to repeat the detection of the event to ensure the idempotency of event processing. At this time, the unique identification of events can be used as the basis for event de-duplication.

Event unique identifier itself has little impact on domain modeling, but it has great benefits for technology processing. Therefore, it is managed as a general property.

2.2 Publishing Domain Events

How can we avoid coupling between domain events and handlers?

A simple and efficient way is to use the observer pattern, which can decouple domain events from external components.

2.2.1 Publish-Subscribe Model

To unify, we need to define a set of interfaces and implementation classes to publish events based on the observer pattern.

The interface and implementation classes are as follows:

Interface or class Meaning
DomainEventPublisher Used for publishing domain events
DomainEventHandlerRegistry Used to register DomainEventHandler
DomainEventBus Extended from Domain Event Publisher and Domain Event Handler Registry for publishing and managing domain event handlers
DefaultDomainEventBus Default implementation of Domain EventBus
DomainEventHandler Used to handle domain events
DomainEventSubscriber Used to determine whether to accept domain events
DomainEventExecutor Event Processor for Domain Execution

The use example is shown in Domain Event Bus Test:

public class DomainEventBusTest {
    private DomainEventBus domainEventBus;

    @Before
    public void setUp() throws Exception {
        this.domainEventBus = new DefaultDomainEventBus();
    }

    @After
    public void tearDown() throws Exception {
        this.domainEventBus = null;
    }

    @Test
    public void publishTest(){
        // Create event handlers
        TestEventHandler eventHandler = new TestEventHandler();
        // Registered Event Processor
        this.domainEventBus.register(TestEvent.class, eventHandler);

        // Release events
        this.domainEventBus.publish(new TestEvent("123"));

        // Detection Event Processor is Running Sufficiently
        Assert.assertEquals("123", eventHandler.data);
    }

    @Value
    class TestEvent extends AbstractDomainEvent{
        private String data;
    }

    class TestEventHandler implements DomainEventHandler<TestEvent>{
        private String data;
        @Override
        public void handle(TestEvent event) {
            this.data = event.getData();
        }
    }
}

After building the publish-subscribe structure, it needs to be associated with the domain model. How does the domain model get Publisher and how does the event handler subscribe?

2.2.2 ThreadLocal-based event Publishing

A common solution is to bind Domain EventBus to thread context. In this way, DomainEventBus objects can be easily accessed as long as they are the same calling thread.

Specific interactions are as follows:

Domain EventBusHolder is used to manage Domain EventBus.

public class DomainEventBusHolder {
    private static final ThreadLocal<DomainEventBus> THREAD_LOCAL = new ThreadLocal<DomainEventBus>(){
        @Override
        protected DomainEventBus initialValue() {
            return new DefaultDomainEventBus();
        }
    };

    public static DomainEventPublisher getPubliser(){
        return THREAD_LOCAL.get();
    }

    public static DomainEventHandlerRegistry getHandlerRegistry(){
        return THREAD_LOCAL.get();
    }

    public static void clean(){
        THREAD_LOCAL.remove();
    }
}

Account enable s are published directly using Domain Event BusHolder.

public class Account extends JpaAggregate {

    public void enable(){
        AccountEnabledEvent event = new AccountEnabledEvent(this);
        DomainEventBusHolder.getPubliser().publish(event);
    }
}

public class AccountEnabledEvent extends AbstractAggregateEvent<Long, Account> {
    public AccountEnabledEvent(Account source) {
        super(source);
    }
}

Account Application completes subscriber registration and business method invocation.

public class AccountApplication extends AbstractApplication {
    private static final Logger LOGGER = LoggerFactory.getLogger(AccountApplication.class);

    @Autowired
    private AccountRepository repository;

    public void enable(Long id){
        // Clean up the previously bound Handler
        DomainEventBusHolder.clean();

        // Register EventHandler
        AccountEnableEventHandler enableEventHandler = new AccountEnableEventHandler();
        DomainEventBusHolder.getHandlerRegistry().register(AccountEnabledEvent.class, enableEventHandler);

        Optional<Account> accountOptional = repository.getById(id);
        if (accountOptional.isPresent()) {
            Account account = accountOptional.get();
            // enable uses Domain Event BusHolder to publish events directly
            account.enable();
            repository.save(account);
        }
    }
    
    class AccountEnableEventHandler implements DomainEventHandler<AccountEnabledEvent>{

        @Override
        public void handle(AccountEnabledEvent event) {
            LOGGER.info("handle enable event");
        }
    }
}
2.2.3 Entity Cache-based Event Publishing

The event is cached in the entity first, and after the entity state is persisted to storage successfully, the event is published.

Specific interactions are as follows:

The example code is as follows:

public class Account extends JpaAggregate {

    public void enable(){
        AccountEnabledEvent event = new AccountEnabledEvent(this);
        registerEvent(event);
    }
}

Account's enable method calls registerEvent to register events.

@MappedSuperclass
public abstract class AbstractAggregate<ID> extends AbstractEntity<ID> implements Aggregate<ID> {
    private static final Logger LOGGER = LoggerFactory.getLogger(AbstractAggregate.class);

    @JsonIgnore
    @QueryTransient
    @Transient
    @org.springframework.data.annotation.Transient
    private final transient List<DomainEventItem> events = Lists.newArrayList();

    protected void registerEvent(DomainEvent event) {
        events.add(new DomainEventItem(event));
    }

    protected void registerEvent(Supplier<DomainEvent> eventSupplier) {
        this.events.add(new DomainEventItem(eventSupplier));
    }

    @Override
    @JsonIgnore
    public List<DomainEvent> getEvents() {
        return Collections.unmodifiableList(events.stream()
                .map(eventSupplier -> eventSupplier.getEvent())
                .collect(Collectors.toList()));
    }

    @Override
    public void cleanEvents() {
        events.clear();
    }


    private class DomainEventItem {
        DomainEventItem(DomainEvent event) {
            Preconditions.checkArgument(event != null);
            this.domainEvent = event;
        }

        DomainEventItem(Supplier<DomainEvent> supplier) {
            Preconditions.checkArgument(supplier != null);
            this.domainEventSupplier = supplier;
        }

        private DomainEvent domainEvent;
        private Supplier<DomainEvent> domainEventSupplier;

        public DomainEvent getEvent() {
            if (domainEvent != null) {
                return domainEvent;
            }
            DomainEvent event = this.domainEventSupplier != null ? this.domainEventSupplier.get() : null;
            domainEvent = event;
            return domainEvent;
        }
    }
}

The registerEvent method is in AbstractAggregate. The registerEvent method saves events to events collection, the getEvents method gets all events, and the cleanEvents method cleans up cached events.

The Application example is as follows:

@Service
public class AccountApplication extends AbstractApplication {
    private static final Logger LOGGER = LoggerFactory.getLogger(AccountApplication.class);

    @Autowired
    private AccountRepository repository;

    @Autowired
    private DomainEventBus domainEventBus;

    @PostConstruct
    public void init(){
        // Registering event handlers using Spring lifecycle
        this.domainEventBus.register(AccountEnabledEvent.class, new AccountEnableEventHandler());
    }

    public void enable(Long id){
        Optional<Account> accountOptional = repository.getById(id);
        if (accountOptional.isPresent()) {
            Account account = accountOptional.get();
            // enable caches events in account
            account.enable();
            repository.save(account);
            List<DomainEvent> events = account.getEvents();
            if (!CollectionUtils.isEmpty(events)){
                // Publish events after successful persistence
                this.domainEventBus.publishAll(events);
            }
        }
    }

    class AccountEnableEventHandler implements DomainEventHandler<AccountEnabledEvent>{

        @Override
        public void handle(AccountEnabledEvent event) {
            LOGGER.info("handle enable event");
        }
    }
}

The init method of Account Application completes the registration of event listeners. After the entity is persisted successfully, the enable method publish es the cached event through the DomainEventBus instance.

2.2.4 Publishing events by callers

Typically, domain events are generated by aggregated command methods and are published after the command methods are successfully executed.
Sometimes, domain events are not generated by command methods in aggregation, but by requests from users.

At this point, we need to model domain events as an aggregation and have our own repository. However, because domain events represent past events, the repository can only do additional operations and can not modify and delete events.

For example, publish user click events.

@Entity
@Data
public class ClickAction extends JpaAggregate implements DomainEvent {
    @Setter(AccessLevel.PRIVATE)
    private Long userId;
    @Setter(AccessLevel.PRIVATE)
    private String menuId;

    public ClickAction(Long userId, String menuId){
        Preconditions.checkArgument(userId != null);
        Preconditions.checkArgument(StringUtils.isNotEmpty(menuId));

        setUserId(userId);
        setMenuId(menuId);
    }

    @Override
    public String id() {
        return String.valueOf(getId());
    }

    @Override
    public Date occurredOn() {
        return getCreateTime();
    }

}

ClickAction inherits from JpaAggregate to implement the DomainEvent interface, and overrides the id and occurrenceredOn methods.

@Service
public class ClickActionApplication extends AbstractApplication {
    @Autowired
    private ClickActionRepository repository;

    @Autowired
    private DomainEventBus domainEventBus;

    public void clickMenu(Long id, String menuId){
        ClickAction clickAction = new ClickAction(id, menuId);
        clickAction.prePersist();
        this.repository.save(clickAction);
        domainEventBus.publish(clickAction);
    }
}

ClickAction Application publishes events using Domain EventBus after successfully saving ClickAction.

2.3 Subscription Domain Events

What components register subscribers to domain events? Most requests are performed by application services and sometimes registered by domain services.

Because application service is the direct customer of domain model, it is an ideal place to register domain event subscribers, that is, before application service invokes domain methods, it completes the subscription of events.

Subscribe based on ThreadLocal:

public void enable(Long id){
    // Clean up the previously bound Handler
    DomainEventBusHolder.clean();

    // Register EventHandler
    AccountEnableEventHandler enableEventHandler = new AccountEnableEventHandler();
    DomainEventBusHolder.getHandlerRegistry().register(AccountEnabledEvent.class, enableEventHandler);

    Optional<Account> accountOptional = repository.getById(id);
    if (accountOptional.isPresent()) {
        Account account = accountOptional.get();
        // enable uses Domain Event BusHolder to publish events directly
        account.enable();
        repository.save(account);
    }
}

Subscribe based on entity caching:

@PostConstruct
public void init(){
    // Registering event handlers using Spring lifecycle
    this.domainEventBus.register(AccountEnabledEvent.class, new AccountEnableEventHandler());
}

public void enable(Long id){
    Optional<Account> accountOptional = repository.getById(id);
    if (accountOptional.isPresent()) {
        Account account = accountOptional.get();
        // enable caches events in account
        account.enable();
        repository.save(account);
        List<DomainEvent> events = account.getEvents();
        if (!CollectionUtils.isEmpty(events)){
            // Publish events after successful persistence
            this.domainEventBus.publishAll(events);
        }
    }
}

2.4 Handling Domain Events

After the event is released, let's take a look at event handling.

2.4.1 Ensure data consistency between aggregates

We usually use domain events to maintain model consistency. There is a principle in aggregation modeling that only one aggregation can be modified in a transaction, and the resulting changes must be run in a separate transaction.

In this case, the dissemination of transactions that need to be handled with caution.

Application services control transactions. Do not modify another instance of aggregation in the event notification process, because this can undermine one of the principles of aggregation: in a transaction, only one aggregation can be modified.

For simple scenarios, we can use a special transaction isolation strategy to isolate modifications to aggregation. The specific process is as follows:

However, the best solution is to use asynchronous processing. And each definer modifies additional aggregation instances in its own separate transaction.

Event subscribers should not execute command methods on another aggregation, because this would undermine the principle of "modifying only a single aggregation instance in a single transaction". The final consistency among all aggregated instances must be handled asynchronously.

For more information, asynchronous processing of domain events.

2.4.2 Replacement batch processing

Batch processing usually requires complex queries and huge transaction support. When domain events are received, the system processes them immediately. Business requirements are not only met faster, but also batch operations are eliminated.

During the non-peak period of the system, batch processing is usually used to maintain the system, such as deleting expired data, creating new objects, notifying users, updating statistical information and so on. These batch processes often require complex queries and huge transaction support.

If we listen for domain events in the system, the system will process them immediately when receiving domain events. In this way, the original batch centralized processing process is dispersed into many small processing units, business needs can also be met faster, users can proceed to the next operation in time.

2.4.3 Implementing Event Source Mode

Maintaining an event store for all domain events in a single bounding context has many benefits.

Storing events can:

  • Event storage is used as a message queue, and then domain events are published through a message facility.
  • Store events for Rest-based event notifications.
  • Check the history of the results generated by the model naming method.
  • Event storage is used for business prediction and analysis.
  • Use events to rebuild aggregated instances.
  • Perform the undo operation of the aggregation.

Event storage is a relatively large topic, which will be explained in a special chapter.

2.4.4 Boundary Context Integration

Boundary context integration based on domain events is mainly composed of message queuing and REST events.

Here, focus on context integration based on message queues.

When using messaging systems in different contexts, we must ensure final consistency. In this case, we need to preserve the final consistency between at least two kinds of storage: the storage used by the domain model and the persistent storage used by the message queue. We must ensure that events are successfully released when persistent domain models are implemented. If there are two different steps, the model may be in an incorrect state.

Generally speaking, there are three ways:

  1. Domain model and Message Sharing persistent storage. In this case, the submission of the model and event is completed in one transaction, thus ensuring the consistency of the two.
  2. Domain models and messages are controlled by global transactions. In this case, the persistent storage used by the model and message can be separated, but the system performance will be degraded.
  3. In domain persistent storage, a special storage area is created to store events (i.e. event storage) to complete the storage of domain and event in local transactions. The event is then sent asynchronously to the message queue through the back-end service.

Generally speaking, the third is a more elegant solution.

When the consistency requirement is not high, events can be sent directly to the message queue through the domain event subscriber. The specific process is as follows:

When the consistency requirement is high, events need to be stored first, then loaded and distributed to the message queue through background threads. The specific process is as follows:

2.5 Asynchronous Processing of Domain Events

Domain events can collaborate with asynchronous workflows, including asynchronous communication using message queues between bounding contexts. Of course, asynchronous processing can also be initiated in the same bounding context.

As a publisher of events, you should not be concerned about asynchronous processing. Exception handling is determined by the event executor.

Domain Event Executor provides support for asynchronous processing.

DomainEventExecutor eventExecutor  =
                new ExecutorBasedDomainEventExecutor("EventHandler", 1, 100);
this.domainEventBus.register(AccountEnabledEvent.class,
        eventExecutor,
        new AccountEnableEventHandler());

Asynchronous processing means abandoning the ACID feature of database transactions and choosing to use final consistency.

2.6 Internal and External Events

Events need to be distinguished when using domain events to avoid technical implementation problems.

It is essential to recognize the difference between internal and external events.

  • Internal events are events within a domain model that are not shared between bounded contexts.
  • External events are events published externally and shared in multiple bounded contexts.

Typically, in a typical business use case, there may be many internal events, but only one or two external events.

2.6.1 Internal events

Internal events exist within the boundaries context, and the boundaries of the boundaries context are protected.

Internal events are limited within a single bounded context boundary, so domain objects can be referenced directly.

public interface AggregateEvent<ID, A extends Aggregate<ID>> extends DomainEvent{
    A source();

    default A getSource(){
        return source();
    }
}

For example, the source in Aggregate Event points to the aggregation that publishes the event.

public class LikeSubmittedEvent extends AbstractAggregateEvent<Long, Like> {
    public LikeSubmittedEvent(Like source) {
        super(source);
    }

    public LikeSubmittedEvent(String id, Like source) {
        super(id, source);
    }
}

The LikeSubmittedEvent class refers directly to the Like aggregation.

2.6.2 External events

External events exist between bounded contexts and are shared by multiple contexts.

Generally, external events only exist as data carriers. Often uses a flat structure and exposes all attributes.

@Data
public class SubmittedEvent {
    private Owner owner;
    private Target target;
}

Submitted Event is a flat structure, mainly encapsulating data.

Because external events are shared by multiple contexts, version management is very important to avoid significant changes affecting its services.

3. Implementing Domain Event Patterns

Domain event is a general pattern, its essence is to add domain concepts to the publish-subscribe pattern.

3.1 Publish-Subscribe Model for Encapsulating Domain Events

Publish-Subscribe is a mature design pattern with high universality. Therefore, it is recommended to encapsulate domain requirements.

For example, direct use geekhalo-ddd Relevant modules.

Define domain events:

@Value
public class LikeCancelledEvent extends AbstractAggregateEvent<Long, Like> {
    public LikeCancelledEvent(Like source) {
        super(source);
    }
}

Subscribe to domain events:

this.domainEventBus.register(LikeCancelledEvent.class, likeCancelledEvent->{
        CanceledEvent canceledEvent = new CanceledEvent();
        canceledEvent.setOwner(likeCancelledEvent.getSource().getOwner());
        canceledEvent.setTarget(likeCancelledEvent.getSource().getTarget());
        this.redisBasedQueue.pushLikeEvent(canceledEvent);
    });

Asynchronous execution domain events:

DomainEventExecutor eventExecutor  =
                new ExecutorBasedDomainEventExecutor("LikeEventHandler", 1, 100);
this.domainEventBus.register(LikeCancelledEvent.class, 
        eventExecutor, 
        likeCancelledEvent->{
            CanceledEvent canceledEvent = new CanceledEvent();
            canceledEvent.setOwner(likeCancelledEvent.getSource().getOwner());
            canceledEvent.setTarget(likeCancelledEvent.getSource().getTarget());
            this.redisBasedQueue.pushLikeEvent(canceledEvent);
    });

3.2 Memory Bus handles internal events and Message Queue handles external events

Memory bus is simple and efficient, and supports synchronous and asynchronous processing schemes. It is more suitable for dealing with complex internal events. Message queue is complex, but it is good at solving inter-service communication problems, and suitable for dealing with external events.

3.3 Use entity to cache domain events

In theory, events should be released only after the successful completion of the business. Therefore, caching domain events in entities and publishing them after completing business operations is a better solution.

Compared with using ThreadLocal to manage subscribers and subscription callbacks in event publish, the event caching scheme has obvious advantages.

3.4 Event Publishing Using IOC Container

IOC containers provide us with many usage functions, including publish-subscribe functions, such as Spring.

Typically, domain models should not rely directly on Spring containers. Therefore, we still use the memory bus in the domain, adding a subscriber to it, and forwarding events from the memory bus to the Spring container.

class SpringEventDispatcher implements ApplicationEventPublisherAware {

    @Autowired
    private DomainEventBus domainEventBus;

    private ApplicationEventPublisher eventPublisher;

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.eventPublisher = applicationEventPublisher;
    }

    @PostConstruct
    public void addListener(){
        this.domainEventBus.register(event->true, event -> {this.eventPublisher.publishEvent(event);});
    }

}

At this point, we can directly use Spring's EventListener mechanism to process domain events.

@Component
public class RedisBasedQueueExporter {
    @Autowired
    private RedisBasedQueue redisBasedQueue;

    @EventListener
    public void handle(LikeSubmittedEvent likeSubmittedEvent){
        SubmittedEvent submittedEvent = new SubmittedEvent();
        submittedEvent.setOwner(likeSubmittedEvent.getSource().getOwner());
        submittedEvent.setTarget(likeSubmittedEvent.getSource().getTarget());
        this.redisBasedQueue.pushLikeEvent(submittedEvent);
    }


    @EventListener
    public void handle(LikeCancelledEvent likeCancelledEvent){
        CanceledEvent canceledEvent = new CanceledEvent();
        canceledEvent.setOwner(likeCancelledEvent.getSource().getOwner());
        canceledEvent.setTarget(likeCancelledEvent.getSource().getTarget());
        this.redisBasedQueue.pushLikeEvent(canceledEvent);
    }

}

4 Summary

  • Domain events are facts that occur in the problem domain and are part of the common language.
  • Domain events use publish-subscribe mode first, which publishes events and triggers corresponding event handlers.
  • Internal events and memory buses are preferred within bounding contexts, while external events and message queues are preferred between bounding contexts.
  • Domain events make asynchronous operations simple.
  • Domain events provide final consistency between aggregates.
  • Domain events can simplify large batch operations to many small business operations.
  • Domain events can perform powerful event storage.
  • Domain events can integrate bounded contexts.
  • Domain events are a support for more complex architecture (CQRS).

Posted by irbrian on Thu, 09 May 2019 14:08:38 -0700