Represent something meaningful that happened inside a single bounded context as a small, in-process event, distinct from cross-service integration events.
An `Order.complete()` method that also sends a confirmation email, updates a loyalty point balance, and notifies a warehouse system directly is now responsible for things far outside what "completing an order" should mean — and every one of those side effects makes the method harder to test and more likely to break for unrelated reasons.
When something meaningful happens inside the domain, the entity raises a domain event (`OrderCompleted`) recording that fact — without deciding what should happen next. Separate handlers, subscribed to that event, carry out the side effects. This is the in-process cousin of Publisher-Subscriber, scoped to a single application rather than across services.
`Order.complete()` only records that completion happened. Everything that should follow from it lives in separate, independently testable handlers.
class Order {
#events = [];
complete() {
this.status = "completed";
this.#events.push({ type: "OrderCompleted", orderId: this.id });
}
pullEvents() {
const events = this.#events;
this.#events = [];
return events;
}
}
const handlers = {
OrderCompleted: [
(e) => console.log(`email: confirmation sent for ${e.orderId}`),
(e) => console.log(`loyalty: points added for ${e.orderId}`),
],
};
function dispatch(events) {
for (const event of events) {
for (const handler of handlers[event.type] ?? []) handler(event);
}
}
const order = new Order();
order.id = "order-1";
order.complete(); // Order itself does nothing but record the fact
dispatch(order.pullEvents()); // side effects happen here, decoupled from Order