Domain-event
Introduction
The domain-event module is a wrapper around Disruptor. It can help your system implement models similar to Guava EventBus and Spring ApplicationEvent for decoupling business logic and reducing concurrency complexity.
Disruptor is an open-source concurrency framework and can be viewed as a lightweight, ultra-fast single-machine MQ. It is a high-performance queue developed by LMAX (a UK FX trading company), greatly simplifying concurrent program development and winning the 2011 Duke's Program Framework Innovation Award.
Domain-event features
- Domain-driven design based on the LMAX architecture.
- Follows the single responsibility principle, enabling high extensibility, high scalability, and low coupling.
- Asynchronous, highly concurrent, thread-safe processing via Disruptor's ring buffer.
- Uses event consumption style coding, which keeps code organized even when business logic becomes complex, with lower maintenance cost.
- Flexible customization of business thread models.
- Plugin-style domain events, fully pluggable and composable.
The module is simple to use. In practice, you only need to focus on two things:
- Define business data carriers (domain messages).
- Handle events for those business data carriers (domain events).
Performance Data
see https://lmax-exchange.github.io/disruptor/user-guide/index.html
| Multiple Producer | |
|---|---|
| Run 0 | Disruptor=26,553,372 ops/sec |
| Run 1 | Disruptor=28,727,377 ops/sec |
| Run 2 | Disruptor=29,806,259 ops/sec |
| Run 3 | Disruptor=29,717,682 ops/sec |
| Run 4 | Disruptor=28,818,443 ops/sec |
| Run 5 | Disruptor=29,103,608 ops/sec |
| Run 6 | Disruptor=29,239,766 ops/sec |
| Single Producer | |
|---|---|
| Run 0 | Disruptor=89,365,504 ops/sec |
| Run 1 | Disruptor=77,579,519 ops/sec |
| Run 2 | Disruptor=78,678,206 ops/sec |
| Run 3 | Disruptor=80,840,743 ops/sec |
| Run 4 | Disruptor=81,037,277 ops/sec |
| Run 5 | Disruptor=81,168,831 ops/sec |
| Run 6 | Disruptor=81,699,346 ops/sec |
Usage Scenarios
Scenario 1
Assume a business scenario where after user registration is completed, you need to trigger both email notification and reward points logic. Without using MQ, what are possible approaches?
Possible approaches include:
- After user registration completes, start threads for email notification and reward points.
- Use disruptor.
- Producer-consumer pattern.
- Observer pattern, etc.
Here we use Disruptor to implement a model similar to Spring ApplicationEvent, referred to here as domain events. Event-driven and observer patterns are similar in some aspects: when a subject changes, dependents are notified. However, the observer pattern is typically tied to a single event source, while event-driven models can be associated with multiple event sources.
Scenario 2
A domain event can be treated like a thread and is recommended for compute-intensive business. For example, in card-playing logic, all tables can share one domain event for play operations. When game settlement involves IO, move settlement to another thread so play threads are not blocked.
How to Install
This module is optional. Add it to pom.xml when needed.
see https://central.sonatype.com/artifact/com.iohao.net/extension-domain-event
<dependency>
<groupId>com.iohao.net</groupId>
<artifactId>extension-domain-event</artifactId>
<version>${ionet.version}</version>
</dependency>
Defining Domain Messages
- Define a domain entity and implement the Eo interface.
- Eo is provided by the framework. Domain messages are user-defined, and using the Eo suffix is recommended.
public record StudentEo(int id) implements Eo {
}
Defining Domain Events
To define a domain event, implement DomainEventHandler.
This interface handles domain messages, and each consumer class should handle one event only (single responsibility principle).
public final class StudentEmailEventHandler1 implements DomainEventHandler<StudentEo> {
@Override
public void onEvent(StudentEo studentEo, boolean endOfBatch) {
log.debug("Sending email to a student: {}", studentEo);
}
}
Test
- code 6: stop domain events.
- code 12: configure domain event - send an email to a student.
- code 13: configure domain event - go home.
- code 14: configure domain event - put the student to sleep.
- code 17: start domain events.
- code 22: publish event.
public class StudentDomainEventTest {
DomainEventApplication domainEventApplication;
@AfterEach
public void tearDown() {
domainEventApplication.stop();
}
@BeforeEach
public void setUp() {
var setting = new DomainEventSetting();
setting.addEventHandler(new StudentEmailEventHandler1());
setting.addEventHandler(new StudentGoHomeEventHandler2());
setting.addEventHandler(new StudentSleepEventHandler3());
domainEventApplication = new DomainEventApplication();
domainEventApplication.startup(setting);
}
@Test
public void testEventSend() {
new StudentEo(1).send();
}
}
- Event publishing is simple; only one event is configured above.
- If you later need to record what courses the student took today, just add another configuration (extensible) with no business code changes (low coupling).
- If you later no longer need the email event, remove its configuration directly, still with no code changes (high scalability).
new StudentEo(1).send();
Providing Domain Events for Third-Party Libraries
We can provide domain events for third-party classes. Assume UserLogin is from a third-party library.
public record UserLogin(int id, String name) {
}
Below is our handler for UserLogin.
...
@Test
public void testEventSend() {
var userLogin = new UserLogin(101, "Michael Jackson");
DomainEventPublish.send(userLogin);
}
public class UserLoginEmailEventHandler implements DomainEventHandler<UserLogin> {
@Override
public void onEvent(UserLogin userLogin, boolean endOfBatch) {
log.info("Sending email to logged-in user {}", userLogin);
}
}