After a short summer break, I’m back to discussing hexagonal architecture, focusing on the crucial role of domain events and how to manage them effectively. Domain events are essential for maintaining separation of concerns and modularity in applications designed with Domain-Driven Design (DDD).
What Are Domain Events?
Domain events represent significant changes in an application’s domain model. In a well-designed system, a domain event signals that an important operation has occurred, making the system more reactive and scalable. For instance, in an e-commerce application, transitioning an order from “CREATED” to “SHIPPED” could trigger various events that notify other systems, update inventories, or activate workflows.
The Order Class and State Transitions
The Order
class manages the state of an order and its transitions, using domain events to notify changes. Here’s how it works:
Structure of the Order Class
@Getter
public class Order extends AggregateRoot<UUID> {
private OrderStatus status;
private BigDecimal price;
private final List<OrderItem> items = new ArrayList<>();
private final Long version;
private final EventStore eventStore;
private static final Map<OrderStatus, Set<OrderStatus>> validTransitions = new EnumMap<>(OrderStatus.class);
static {
validTransitions.put(OrderStatus.NEW, EnumSet.of(OrderStatus.CREATED));
validTransitions.put(OrderStatus.CREATED, EnumSet.of(OrderStatus.SHIPPED, OrderStatus.CANCELLED));
validTransitions.put(OrderStatus.SHIPPED, EnumSet.of(OrderStatus.DELIVERED));
validTransitions.put(OrderStatus.DELIVERED, EnumSet.noneOf(OrderStatus.class));
validTransitions.put(OrderStatus.CANCELLED, EnumSet.noneOf(OrderStatus.class));
}
public Order(UUID id, List<OrderItem> items, OrderStatus status, BigDecimal price, Long version) {
super(id);
this.items.addAll(items);
this.status = status;
this.price = price;
this.version = version;
this.eventStore = new EventStore();
}
public void created() { changeStatus(OrderStatus.CREATED); }
public void cancelled() { changeStatus(OrderStatus.CANCELLED); }
public void shipped() { changeStatus(OrderStatus.SHIPPED); }
public void delivered() { changeStatus(OrderStatus.DELIVERED); }
private void changeStatus(OrderStatus orderStatus) {
Set<OrderStatus> allowedStatuses = validTransitions.get(this.status);
if (allowedStatuses == null || !allowedStatuses.contains(orderStatus)) {
throw new DomainException("Cannot change status from " + this.status + " to " + orderStatus);
}
this.status = orderStatus;
triggerEvent();
}
private void triggerEvent() {
OrderEventFactory factory = new AllOrderEventFactory();
registerEvent(
factory.createEvent( this )
);
}
}
State Transitions and Validation
The Order
class defines valid state transitions through the validTransitions
map. This approach ensures that an order’s state can only change following defined paths. For example, a CREATED state can transition to SHIPPED or CANCELLED, but not directly to DELIVERED.
When the state changes, the changeStatus() method performs validation to ensure the transition is allowed. If valid, triggerEvent() is called to generate a domain event.
The Importance of EventStore
The EventStore is a crucial component for keeping track of domain events. Every time an order’s state changes, an event is created via the AllOrderEventFactory
and registered in the EventStore
. This registration is essential for several reasons:
Traceability and Auditing: Events registered in the EventStore
provide a complete history of state changes, useful for audits and debugging.
System Reactivity: Other components can be notified of registered events, enhancing integration and system reactivity. For example, a billing system might listen for SHIPPED
order events to generate invoices.
Decoupling: By using an EventStore
, different system components can be decoupled. Each component can react to events independently, making scaling and maintenance easier.
@Getter
public final class EventStore {
private final Queue< DomainEvent > events = new ConcurrentLinkedQueue<>();
public void addEvent( DomainEvent event ){
events.add( event );
}
public void clear(){
events.clear();
}
}
Creating Events with AllOrderEventFactory
The AllOrderEventFactory
adheres to the Single Responsibility Principle: it is solely responsible for creating domain events based on the order’s state. This approach keeps business logic separate from event creation logic, maintaining code cohesion.
public interface OrderEventFactory {
DomainEvent createEvent( Order order );
}
Conclusion
Integrating domain events with an effective EventStore is essential for building modular and scalable systems in hexagonal architecture. They provide a robust way to track significant changes in the domain and facilitate integration with other systems. In future articles, we will explore how these events can be leveraged to further improve software quality and responsiveness.
Comments are closed