Decoupling Logic with Domain Events [Guide] - Domain-Driven Design w/ TypeScript

We cover this topic in The Software Essentialist online course. Check it out if you liked this post.
Introduction
When we're working on backend logic, it's not uncommon to eventually find yourself using language like this to describe what should happen.
"When __ happens, then do ___".
And "after this, do that".
Or, "after this, and that, and then this... but only when this, do that".
Just all kinds of noise, right?
Chaining logic can get really messy.
Here's what I mean.
When someone registers for an account on White Label, the open-source Vinyl-Trading DDD application I'm building, I want to do several things.
I want to:
- Create the user's account
- Uniquely assign that user a default username based on their actual name
- Send a notification to my Slack channel letting me know that someone signed up
- Add that user to my Mailing List
- Send them a Welcome Email
... and I'll probably think of more things later as well.
How would you code this up?
A first approach might be to do all of this stuff in a UsersService
, because that's where the main event (creating the User
) is taking place, right?
import { SlackService } from '../../notification/services/slack';
import { MailchimpService } from '../../marketing/services/mailchimp';
import { SendGridService } from '../../notification/services/email';
class UsersService {
private sequelizeModels: any;
constuctor (sequelizeModels: any) {
this.sequelizeModels = sequelizeModels;
}
async createUser (
email: string, password: string, firstName: string, lastName: string
): Promise<void> {
try {
// Assign a username (also, might be taken)
const username = `${firstName}${lastName}`
// Create user
await sequelizeModels.User.create({
email, password, firstName, lastName, username
});
// Send notification to slack channel
await SlackService.sendNotificatation (
`Hey guys, ${firstName} ${lastName} @ ${email} just signed up.`
);
// Add user to mailing list
await MailchimpService.addEmail(email)
// Send a welcome email
const welcomeEmailTitle = `Welcome to White Label`
const welcomeEmailText = `Hey, welcome to the hottest place to trade vinyl.`
await SendGridService.sendEmail(email, welcomeEmailTitle, welcomeEmailText);
} catch (err) {
console.log(err);
}
}
}
The humanity! You probably feel like this right now.
Alright, what's wrong with this?
Lots. But the main things are:
- The
UsersService
knows too much about things that aren't related toUsers
. Sending emails and slack messages most likely should belong to theNotifications
subdomain, while hooking up marketing campaigns using a tool like Mailchimp would make more sense to belong to aMarketing
subdomain. Currently, we've coupled all of the unrelated side-effects ofcreateUser
to theUsersService
. Think about how challenging it will be in order to isolate and test this class now.
We can fix this.
There's a design principle out there that's specifically useful for times like this.

Design Principle: "Strive for loosely coupled design against objects that interact". - via solidbook.io: The Software Architecture & Design Handbook
This decoupling principle is at the heart of lots of one of my favourite libraries, RxJs.
The design pattern it's built on is called the observer pattern.
In this article, we'll learn how to use Domain Events in order to decouple complex business logic.
Prerequisites
In order to get the most out of this guide, you should know the following:
- What Domain-Driven Design is all about.
- Understand the role of entities, value objects, aggregates, and repositories.
- How logically separating your app into Subdomains and Use Cases helps you to quickly understand where code belongs and enforce architectural boundaries.
- And (optionally), you've read the previous article on Domain Events.
What are Domain Events?
Every business has key events that are important.
In our vinyl-trading application, within the vinyl
subdomain, we have events like VinylCreated
, VinylUpdated
, and VinylAddedToWishList
.
In a job seeking application, we might see events like JobPosted
, AppliedToJob
, or JobExpired
.
Despite the domain that they belong to, when domain events are created and dispatched, they provide the opportunity for other (decoupled) parts of our application to execute some code after that event.
Actors, Commands, Events, and Subscriptions
In order to determine all of the capabilities of an app, an approach is to start by identifying the actors, commands, events, and responses to those events (subscriptions).
- Actors: Who or what is this relevant person (or thing) to the domain? -
Authors
,Editors
,Guest
,Server
- Commands: What can they do? -
CreateUser
,DeleteAccount
,PostArticle
- Event: Past-tense version of the command (verb) -
UserCreated
,AccountDeleted
,ArticlePosted
- Subscriptions: Classes that are interested in the domain event that want to be notified when they occurred -
AfterUserCreated
,AfterAccountDeleted
,AfterArticlePosted
In the DDD-world, there's a fun activity you can do with your team to discover all of these. It's called Event Storming and it involves using sticky notes in order to discover the business rules.

Each sticky note color represents a different DDD concept.
At the end of this process, depending on the size of your company, you'll probably end up with a huge board full of stickies.

Someone's productive Event Storming session.
At this point, we'll probably have a good understanding of who does what, what the commands are, what the policies are the govern when someone can perform a particular command, and what happens in response to those commands as subscriptions to domain events.
Let's apply that to our CreateUser
command at a smaller scale.
Uncovering the business rules
Alright, so any anonymous user is able to create an account. So an anonymous user
should be able to execute the CreateUser
command.
The subdomain this command belongs to would be the Users
subdomain.
Don't remember how subdomains work? Read this article first.
OK. Now, what are the other things we want to happen in response to the UserCreated
event that would get created and then dispatched?
Let's look at the code again.
import { SlackService } from '../../notification/services/slack';
import { MailchimpService } from '../../marketing/services/mailchimp';
import { SendGridService } from '../../notification/services/email';
class UsersService {
private sequelizeModels: any;
constuctor (sequelizeModels: any) {
this.sequelizeModels = sequelizeModels;
}
async createUser (
email: string, password: string, firstName: string, lastName: string
): Promise<void> {
try {
// Create user (this is ALL that should be here)
await sequelizeModels.User.create({
email, password, firstName, lastName
});
// Subscription side-effect (Users subdomain): Assign user username
// Subscription side-effect (Notifications subdomain): Send notification to slack channel
// Subscription side-effect (Marketing subdomain): Add user to mailing list
// Subscription side-effect (Notifications subdomain): Send a welcome email
} catch (err) {
console.log(err);
}
}
}
Alright, so we have:
- Subscription side-effect #1:
Users
subdomain - Assign user username - Subscription side-effect #2:
Notifications
subdomain - Send notification to slack channel - Subscription side-effect #3:
Marketing
subdomain - Add user to mailing list - Subscription side-effect #4:
Notifications
subdomain - Send a welcome email
Great, so if were were to visualize the subdomains as modules (folders) in a monolith codebase, this is what the generalization would look like:
Actually, we're missing something.
Since we need to assign a username to the user after the UserCreated
event (and since that operation belongs to the Users
subdomain), the visualization would look more like this:
Yeah. Sounds like a good plan. And let's start from scratch instead of using this anemic UsersService
.
Want to skip to the finished product? Fork the repo for White Label, here.
An explicit Domain Event interface
We'll need an interface in order to depict what a domain event looks like. It won't need much other than the time it was created at, and a way to get the aggregate id.
import { UniqueEntityID } from "../../core/types";
export interface IDomainEvent {
dateTimeOccurred: Date;
getAggregateId (): UniqueEntityID;
}
It's more of an intention revealing interface than anything that actually does something. Half the battle in fighting confusing and complex code is using good names for things.
How to define Domain Events
A domain event is a "plain ol' TypeScript object". Not much to it other than it needs to implement the interface which means providing the date, the getAggregateId (): UniqueEntityID
method and any other contextual information that might be useful for someone who subscribes to this domain event to know about.
In this case, I'm passing in the entire User
aggregate.
Some will advise against this, but for this simple example, you should be OK.
import { IDomainEvent } from "../../../../core/domain/events/IDomainEvent";
import { UniqueEntityID } from "../../../../core/domain/UniqueEntityID";
import { User } from "../user";
export class UserCreated implements IDomainEvent {
public dateTimeOccurred: Date;
public user: User;
constructor (user: User) {
this.dateTimeOccurred = new Date();
this.user = user;
}
getAggregateId (): UniqueEntityID {
return this.user.id;
}
}
How to create domain events
Here's where it gets interesting. The following class is the User
aggregate root. Read through it. I've commented the interesting parts.
import { AggregateRoot } from "../../../core/domain/AggregateRoot";
import { UniqueEntityID } from "../../../core/domain/UniqueEntityID";
import { Result } from "../../../core/logic/Result";
import { UserId } from "./userId";
import { UserEmail } from "./userEmail";
import { Guard } from "../../../core/logic/Guard";
import { UserCreatedEvent } from "./events/userCreatedEvent";
import { UserPassword } from "./userPassword";
// In order to create one of these, you need to pass
// in all of these props. Non-primitive types are Value Objects
// that encapsulate their own validation rules.
interface UserProps {
firstName: string;
lastName: string;
email: UserEmail;
password: UserPassword;
isEmailVerified: boolean;
profilePicture?: string;
googleId?: number; // Users can register w/ google
facebookId?: number; // and facebook (instead of email + pass)
username?: string;
}
// User is a subclass of AggregateRoot. We'll look at the AggregateRoot
// class again shortly.
export class User extends AggregateRoot<UserProps> {
get id (): UniqueEntityID {
return this._id;
}
get userId (): UserId {
return UserId.caller(this.id)
}
get email (): UserEmail {
return this.props.email;
}
get firstName (): string {
return this.props.firstName
}
get lastName (): string {
return this.props.lastName;
}
get password (): UserPassword {
return this.props.password;
}
get isEmailVerified (): boolean {
return this.props.isEmailVerified;
}
get profilePicture (): string {
return this.props.profilePicture;
}
get googleId (): number {
return this.props.googleId;
}
get facebookId (): number {
return this.props.facebookId;
}
get username (): string {
return this.props.username;
}
// Notice that there aren't setters for everything?
// There are only setters for things that it makes sense
// for there for be setters for, like `username`.
set username (value: string) {
this.props.username = value;
}
// The constructor is private so that it forces you to use the
// `create` Factory method. There's no way to circumvent
// validation rules that way.
private constructor (props: UserProps, id?: UniqueEntityID) {
super(props, id);
}
private static isRegisteringWithGoogle (props: UserProps): boolean {
return !!props.googleId === true;
}
private static isRegisteringWithFacebook (props: UserProps): boolean {
return !!props.facebookId === true;
}
public static create (props: UserProps, id?: UniqueEntityID): Result<User> {
// Here are things that cannot be null
const guardedProps = [
{ argument: props.firstName, argumentName: 'firstName' },
{ argument: props.lastName, argumentName: 'lastName' },
{ argument: props.email, argumentName: 'email' },
{ argument: props.isEmailVerified, argumentName: 'isEmailVerified' }
];
if (
!this.isRegisteringWithGoogle(props) &&
!this.isRegisteringWithFacebook(props)
) {
// If we're not registering w/ a social provider, we also
// need `password`.
guardedProps.push({ argument: props.password, argumentName: 'password' })
}
// Utility that checks if anything is missing
const guardResult = Guard.againstNullOrUndefinedBulk(guardedProps);
if (!guardResult.succeeded) {
return Result.fail<User>(guardResult.message)
}
else {
// Create the user object and set any default values
const user = new User({
...props,
username: props.username ? props.username : '',
}, id);
// If the id wasn't provided, it means that we're creating a new
// user, so we should create a UserCreatedEvent.
const idWasProvided = !!id;
if (!idWasProvided) {
// Method from the AggregateRoot parent class. We'll look
// closer at this.
user.addDomainEvent(new UserCreated(user));
}
return Result.ok<User>(user);
}
}
}
View this file on GitHub.
When we use the factory method to create the User, depending on if the User is new (meaning it doesn't have an identifier yet) or it's old (and we're just reconsistuting it from persistence), we'll create the UserCreated
domain event.
Let's look a little closer at what happens when we do user.addDomainEvent(new UserCreated(user));
.
That's where we're creating/raising the domain event.
We need to go to the AggregateRoot
class to see what we do with this.
Handling created/raised domain events
If you remember from our previous chats about aggregates and aggregate roots, the aggregate root in DDD is the domain object that we use to perform transactions.
It's the object that we refer to from the outside in order to change anything within it's invariant boundary.
That means that anytime a transaction that wants to change the aggregate in some way (ie: a command getting executed), it's the aggregate that is responsible for ensuring that all the business rules are satified on that object and it's not in an invalid state.
It says,
"Yes, all good! All my invariants are satisfied, you can go ahead and save now."
Or it might say,
"Ah, no- you're not allowed to add more than 3
Genres
to aVinyl
aggregate. Not OK."
Hopefully, none of that is new as we've talked about that on the blog already.
What's new is how we handle those created/raised domain events.
Here's the aggregate root class.
Check out the protected addDomainEvent (domainEvent: IDomainEvent): void
method.
import { Entity } from "./Entity";
import { IDomainEvent } from "./events/IDomainEvent";
import { DomainEvents } from "./events/DomainEvents";
import { UniqueEntityID } from "./UniqueEntityID";
// Aggregate root is an `abstract` class because, well- there's
// no such thing as a aggregate in and of itself. It needs to _be_
// something, like User, Vinyl, etc.
export abstract class AggregateRoot<T> extends Entity<T> {
// A list of domain events that occurred on this aggregate
// so far.
private _domainEvents: IDomainEvent[] = [];
get id (): UniqueEntityID {
return this._id;
}
get domainEvents(): IDomainEvent[] {
return this._domainEvents;
}
protected addDomainEvent (domainEvent: IDomainEvent): void {
// Add the domain event to this aggregate's list of domain events
this._domainEvents.push(domainEvent);
// Add this aggregate instance to the DomainEventHandler's list of
// 'dirtied' aggregates
DomainEvents.markAggregateForDispatch(this);
}
public clearEvents (): void {
this._domainEvents.splice(0, this._domainEvents.length);
}
private logDomainEventAdded (domainEvent: IDomainEvent): void {
...
}
}
When we call addDomainEvent(domainEvent: IDomainEvent)
, we:
- add that domain event to a list of events that this aggregate has seen so far, and
- we tell something called
DomainEvents
to markthis
for dispatch.
Almost there, let's see how the DomainEvents
class handles domain events.
The handler of domain events (DomainEvents class)
This was pretty tricky.
My implementation of this is something I ported to TypeScript from Udi Dahan's 2009 blog post about Domain Events in C#.
Here it is in it's entirety.
import { IDomainEvent } from "./IDomainEvent";
import { AggregateRoot } from "../AggregateRoot";
import { UniqueEntityID } from "../UniqueEntityID";
export class DomainEvents {
private static handlersMap = {};
private static markedAggregates: AggregateRoot<any>[] = [];
/**
* @method markAggregateForDispatch
* @static
* @desc Called by aggregate root objects that have created domain
* events to eventually be dispatched when the infrastructure commits
* the unit of work.
*/
public static markAggregateForDispatch (aggregate: AggregateRoot<any>): void {
const aggregateFound = !!this.findMarkedAggregateByID(aggregate.id);
if (!aggregateFound) {
this.markedAggregates.push(aggregate);
}
}
/**
* @method dispatchAggregateEvents
* @static
* @private
* @desc Call all of the handlers for any domain events on this aggregate.
*/
private static dispatchAggregateEvents (aggregate: AggregateRoot<any>): void {
aggregate.domainEvents.forEach((event: IDomainEvent) => this.dispatch(event));
}
/**
* @method removeAggregateFromMarkedDispatchList
* @static
* @desc Removes an aggregate from the marked list.
*/
private static removeAggregateFromMarkedDispatchList (aggregate: AggregateRoot<any>): void {
const index = this.markedAggregates
.findIndex((a) => a.equals(aggregate));
this.markedAggregates.splice(index, 1);
}
/**
* @method findMarkedAggregateByID
* @static
* @desc Finds an aggregate within the list of marked aggregates.
*/
private static findMarkedAggregateByID (id: UniqueEntityID): AggregateRoot<any> {
let found: AggregateRoot<any> = null;
for (let aggregate of this.markedAggregates) {
if (aggregate.id.equals(id)) {
found = aggregate;
}
}
return found;
}
/**
* @method dispatchEventsForAggregate
* @static
* @desc When all we know is the ID of the aggregate, call this
* in order to dispatch any handlers subscribed to events on the
* aggregate.
*/
public static dispatchEventsForAggregate (id: UniqueEntityID): void {
const aggregate = this.findMarkedAggregateByID(id);
if (aggregate) {
this.dispatchAggregateEvents(aggregate);
aggregate.clearEvents();
this.removeAggregateFromMarkedDispatchList(aggregate);
}
}
/**
* @method register
* @static
* @desc Register a handler to a domain event.
*/
public static register(
callback: (event: IDomainEvent) => void,
eventClassName: string
): void {
if (!this.handlersMap.hasOwnProperty(eventClassName)) {
this.handlersMap[eventClassName] = [];
}
this.handlersMap[eventClassName].push(callback);
}
/**
* @method clearHandlers
* @static
* @desc Useful for testing.
*/
public static clearHandlers(): void {
this.handlersMap = {};
}
/**
* @method clearMarkedAggregates
* @static
* @desc Useful for testing.
*/
public static clearMarkedAggregates(): void {
this.markedAggregates = [];
}
/**
* @method dispatch
* @static
* @desc Invokes all of the subscribers to a particular domain event.
*/
private static dispatch (event: IDomainEvent): void {
const eventClassName: string = event.constructor.name;
if (this.handlersMap.hasOwnProperty(eventClassName)) {
const handlers: any[] = this.handlersMap[eventClassName];
for (let handler of handlers) {
handler(event);
}
}
}
}
How to register a handler to a Domain Event?
To register a handler to Domain Event, we use the static register
method.
export class DomainEvents {
private static handlersMap = {};
private static markedAggregates: AggregateRoot<any>[] = [];
...
public static register(
callback: (event: IDomainEvent) => void,
eventClassName: string
): void {
if (!this.handlersMap.hasOwnProperty(eventClassName)) {
this.handlersMap[eventClassName] = [];
}
this.handlersMap[eventClassName].push(callback);
}
...
}
It accepts both a callback
function and the eventClassName
, which is the name of the class (we can get that using Class.name
).
When we register a handler for a domain event, it gets added to the handlersMap
.
For 3 different domain events and 7 different handlers, the data structure for the handler's map can end up looking like this:
{
"UserCreated": [Function, Function, Function],
"UserEdited": [Function, Function],
"VinylCreated": [Function, Function]
}
The handlersMap is an Identity map of Domain Event names to callback functions.
How 'bout an example of a handler?
AfterUserCreated subscriber (notifications subdomain)
Remember when mentioned that we want a subscriber from within the Notifications
subdomain to send us a Slack message when someone signs up?
Here's an example of an AfterUserCreated
subscriber setting up a handler to the UserCreated
event from within the User
subdomain.
import { IHandle } from "../../../core/domain/events/IHandle";
import { DomainEvents } from "../../../core/domain/events/DomainEvents";
import { UserCreatedEvent } from "../../users/domain/events/userCreatedEvent";
import { NotifySlackChannel } from "../useCases/notifySlackChannel/NotifySlackChannel";
import { User } from "../../users/domain/user";
export class AfterUserCreated implements IHandle<UserCreated> {
private notifySlackChannel: NotifySlackChannel;
constructor (notifySlackChannel: NotifySlackChannel) {
this.setupSubscriptions();
this.notifySlackChannel = notifySlackChannel;
}
setupSubscriptions(): void {
// Register to the domain event
DomainEvents.register(this.onUserCreated.bind(this), UserCreated.name);
}
private craftSlackMessage (user: User): string {
return `Hey! Guess who just joined us? => ${user.firstName} ${user.lastName}\n
Need to reach 'em? Their email is ${user.email}.`
}
// This is called when the domain event is dispatched.
private async onUserCreatedEvent (event: UserCreated): Promise<void> {
const { user } = event;
try {
await this.notifySlackChannel.execute({
channel: 'growth',
message: this.craftSlackMessage(user)
})
} catch (err) {
}
}
}
View it on GitHub: Psst. It's here too. Check out the folder structure to see all the use cases.
The loose coupling that happens here's is awesome. It leaves the responsibility of keeping track of who needs to be alerted when a domain event is dispatched, to the DomainEvents
class, and removes the need for us to couple code between Users
and Notifications
directly.
Not only is that good practice, it might very well be necessary! Like, when we get into designing microservices.
Microservices
When we've split our application not only logically, but physically as well (via microservices), it's actually impossible for us to couple two different subdomains together.
We should be mindful of that when we're working on monolith codebases that we might want to someday graduate to microservives.
Be mindful of those architectural boundaries between subdomains. They should know very little about each other.
How does it work in a real-life transaction?
So we've seen how to register a handler from a subscriber to a domain event.
And we've seen how an aggregate root can create, pass, and store the domain event in an array within the DomainEvents
class using addDomainEvent(domainEvent: IDomainEvent)
until it's ready to be dispatched.
markedAggregates = [User, Vinyl]
What are we missing?
At this point, there are a few more questions I had:
- How do we handle failed transactions? What if we tried to execute the
CreateUser
use case, but it failed before the transaction succeeded? It looks like the domain event still gets created. How do we prevent it from getting sent off to subscribers if the transaction fails? Do we need a Unit of Work pattern? - Who's responsibility is it to dictate when the domain event should be sent to all subscribers? Who calls
dispatchEventsForAggregate(id: UniqueEntityId)
?
Separating the creation from the dispatch of the domain event
When a domain event is created, it's not dispatched right away.
That domain event goes onto the aggregate, then the aggregate gets marked in DomainEvents
array.
console.log(user.domainEvents) // [UserCreated]
The DomainEvents
class then waits until something tells it to dispatch all the handlers within the markedAggregates
array that match a particular aggregate id.
The question is, who's responsibility is it to say when the transaction was successful?
Your ORM is the single source of truth for a successful transaction
That's right.
The thing is, a lot of these ORMs actually have mechanisms built in to execute code after things get saved to the database.
For example, the Sequelize docs has hooks for each of these lifecycle events.
(1)
beforeBulkCreate(instances, options)
beforeBulkDestroy(options)
beforeBulkUpdate(options)
(2)
beforeValidate(instance, options)
(-)
validate
(3)
afterValidate(instance, options)
- or -
validationFailed(instance, options, error)
(4)
beforeCreate(instance, options)
beforeDestroy(instance, options)
beforeUpdate(instance, options)
beforeSave(instance, options)
beforeUpsert(values, options)
(-)
create
destroy
update
(5)
afterCreate(instance, options)
afterDestroy(instance, options)
afterUpdate(instance, options)
afterSave(instance, options)
afterUpsert(created, options)
(6)
afterBulkCreate(instances, options)
afterBulkDestroy(options)
afterBulkUpdate(options)
We're interested in the ones in (5).
And TypeORM has a bunch entity listeners which are effectively the same thing.
- @AfterLoad
- @BeforeInsert
- @AfterInsert
- @BeforeUpdate
- @AfterUpdate
- @BeforeRemove
- @AfterRemove
Again, we're mostly interested in the ones that happen afterwards.
For example, if the CreateUserUseCase
like the one shown below transaction suceeds, it's right after the repository is able to create or update the User
that the hook gets invoked.
import { UseCase } from "../../../../core/domain/UseCase";
import { CreateUserDTO } from "./CreateUserDTO";
import { Either, Result, left, right } from "../../../../core/logic/Result";
import { UserEmail } from "../../domain/userEmail";
import { UserPassword } from "../../domain/userPassword";
import { User } from "../../domain/user";
import { IUserRepo } from "../../repos/userRepo";
import { CreateUserErrors } from "./CreateUserErrors";
import { GenericAppError } from "../../../../core/logic/AppError";
type Response = Either<
GenericAppError.UnexpectedError |
CreateUserErrors.AccountAlreadyExists |
Result<any>,
Result<void>
>
export class CreateUserUseCase implements UseCase<CreateUserDTO, Promise<Response>> {
private userRepo: IUserRepo;
constructor (userRepo: IUserRepo) {
this.userRepo = userRepo;
}
async execute (req: CreateUserDTO): Promise<Response> {
const { firstName, lastName } = req;
const emailOrError = UserEmail.create(req.email);
const passwordOrError = UserPassword.create({ value: req.password });
const combinedPropsResult = Result.combine([
emailOrError, passwordOrError
]);
if (combinedPropsResult.isFailure) {
return left(
Result.fail<void>(combinedPropsResult.error)
) as Response;
}
// Domain event gets created internally, here!
const userOrError = User.create({
email: emailOrError.getValue(),
password: passwordOrError.getValue(),
firstName,
lastName,
isEmailVerified: false
});
if (userOrError.isFailure) {
return left(
Result.fail<void>(combinedPropsResult.error)
) as Response;
}
const user: User = userOrError.getValue();
const userAlreadyExists = await this.userRepo.exists(user.email);
if (userAlreadyExists) {
return left(
new CreateUserErrors.AccountAlreadyExists(user.email.value)
) as Response;
}
try {
// If this transaction succeeds, we the afterCreate or afterUpdate hooks
// get called.
await this.userRepo.save(user);
} catch (err) {
return left(new GenericAppError.UnexpectedError(err)) as Response;
}
return right(Result.ok<void>()) as Response;
}
}
Hooking into succesful transactions with Sequelize
Using Sequelize, we can define a callback function for each hook that takes the model name and the primary key field in order to dispatch the events for the aggregate.
import models from '../models';
import { DomainEvents } from '../../../core/domain/events/DomainEvents';
import { UniqueEntityID } from '../../../core/domain/UniqueEntityID';
const dispatchEventsCallback = (model: any, primaryKeyField: string) => {
const aggregateId = new UniqueEntityID(model[primaryKeyField]);
DomainEvents.dispatchEventsForAggregate(aggregateId);
}
(async function createHooksForAggregateRoots () {
const { BaseUser } = models;
BaseUser.addHook('afterCreate', (m: any) => dispatchEventsCallback(m, 'base_user_id'));
BaseUser.addHook('afterDestroy', (m: any) => dispatchEventsCallback(m, 'base_user_id'));
BaseUser.addHook('afterUpdate', (m: any) => dispatchEventsCallback(m, 'base_user_id'));
BaseUser.addHook('afterSave', (m: any) => dispatchEventsCallback(m, 'base_user_id'));
BaseUser.addHook('afterUpsert', (m: any) => dispatchEventsCallback(m, 'base_user_id'));
})();
Hooks not running?: To ensure your Sequelize hooks always run, use the hooks: true option as described in "Ensuring Sequelize Hooks Always Get Run".
Hooking into succesful transactions with TypeORM
Using TypeORM, here's how we can utilize the entity listener decorators to accomplish the same thing.
@Entity()
export class User {
@AfterUpdate()
dispatchAggregateEvents() {
const aggregateId = new UniqueEntityID(this.userId);
DomainEvents.dispatchEventsForAggregate(aggregateId);
}
}
Conclusion
In this article, we learned:
- How domain logic that belongs to separate subdomains can get coupled
- How to create a basic domain events class
- How we can separate the process of notifying a subscriber to a domain event into 2 parts: creation and dispatch, and why it makes sense to do that.
- How to utilize the your ORM from the infrastructure layer to finalize the dispatch of handlers for your domain events
Want to see the code?: Check it out here.
Now go rule out there and rule the world.
Discussion
Liked this? Sing it loud and proud đ¨âđ¤.
Stay in touch!
Enjoying so far? Join 15000+ Software Essentialists getting my posts delivered straight to your inbox each week. I won't spam ya. đ
View more in Domain-Driven Design
You may also enjoy...
A few more related articles




Want to be notified when new content comes out?
Join 15000+ other Software Essentialists learning how to master The Essentials of software design and architecture.
18 Comments
Commenting has been disabled for now. To ask questions and discuss this post, join the community.
Hi Khalil,
Thanks for the great post, it really helps to see things from the DDD perspective. I've got two questions regarding this article:
1. Is it a good practice to hook up the dispatch of the domain events in the infrastructure layer? This solution makes you depend on the current choose of ORM. Wouldn't it be better to finalize the dispatch from the UseCase after userRepo.save succeeded?
2. How would you handle custom events, let's say if you wanted to send a coupon to the user after his/her 5th successful vinyl trade?
Hey Abel,
Glad you enjoyed the post. Those are good questions!
1. UseCases are application layer concerns, right? At which layer would you say the concerns of a transaction belongs to? The infrastructure layer, right? Since a transaction is only complete once changes have been made to persistence.
You could argue that a Use Case is either a COMMAND or a QUERY, and COMMANDs are transactions, theoretically.
But if we think about what it would mean to need to call `DomainEvents.dispatchEventsForAggregate()` at the end of every use case, it could be easy to forget to do that.
This way, we hook it up once, in the infrastructure layer, for every aggregate root, and we can trust that it will notify our observers/subscribers.
2. Love this question. This is exactly the type of thing that Domain Events were meant to help you do. It's also a kind of small domain logic change request that's not uncommon to come up randomly.
Let's say that when a trade happens between two `Trader`s, the event dispatched is called `TradeAccepted`.
That event lives within the `trading` subdomain.
Now, about this coupon. I'm going to make an assumption that `Coupon` is an entity within a `billing` subdomain that can get added to an `Account`.
An `Account` is the aggregate root and equivalent of a `Trader` from within the `trading` subdomain.
Within the `billing` subdomain, I'd create a subscriber (like we did in this article) called AfterTradeAccepted, that reached into the `TradeAccepted` event to get the ids of the two `Trader`s, and then used a Dependency Injected `traderRepo` to get a read model, perhaps called TraderStats, using traderRepo.getTraderStats(traderId: TraderId).
With a `TraderStats` domain object that has totalNumberTrades as an attribute, you could then figure out if it was appropriate to add a coupon to the account.
We haven't gotten Domain Services or Read Models very much on the blog yet, but they will help to better encapsulate this logic.
For the moment, you can get away with doing most of it within the AfterTradeAccepted subscriber and passing off control to an `AddCouponToAccount` use case from within the `billing` subdomain.
Thanks for the post. Exactly what we are looking for..
Got a question.
Can we have array of entities within aggregateroot?
This is my aggregate.
interface UserProfileProps {
userid: string;
  userOrg: UserOrg[];
}
export default class UserProfile extends AggregateRoot<UserProfileProps> {}
Lets say i have 4 userOrgs
I want to generate an event after insertion of each userOrg.
i.e @AfterInsert ... But what happens is the entire aggregate is generated after each insert( Actually, it should generate the event after inserting all 4 userorgs)
Thank you for this great post!
I like the question @Abel raised regarding the use of DomainEvents in the ORM. In my opinion, dispatching events in the domain service (i.e. the use-case) makes more sense for two reasons:
Regarding @Abel second question, I really liked your response. It helped foster my thinking about domains. I have one follow-up question: shouldn't every domain implement it's own repository/ORM? One could make the point that it's not a good design decision for billing to dependent on the traderRepo, because if you're going to microservices (split by domain), then you'd have to create a dependency between your billing domain and your trading domain. What do you think about this point?
What I really like about your post is that your mixing Uncle Bob's Clean Architecture with Domain Driven Design. I'm actually working on an application that does the same thing and I'm also using Typescript.
Thanks for the great comment!
Reading your response, it's apparent that you and I have a few fundamental disagreements about DDD and Clean Architecture constructs.
DDD is an incredibly challenging topic to fully agree upon the responsibilities of the constructs used. What's a Domain Service? What's an Application Service? What's a "use case" (clean architecture term, not necessarily a DDD term).
Here are my thoughts on some of the ideas and comments you raised (which are very constructive, btw).
"In my opinion, dispatching events in the domain service (i.e. the use-case) makes more sense for two reasons"
Use Cases (Clean Architecture) are to Application Services in DDD, not Domain Services.
My rationale for that comes from the process of exclusion based on the definitions of the terms: Entities, Use Cases
Clean Architecture Definitions
Entities (CA) - "An Entity is an object within our computer system that embodies a small set of critical business rules operating on Critical Business Data. The Entity object either contains the Critical Business Data or has very easy access to that data. The interface of the Entity consists of the functions that implement the Critical Business Rules that operate on that data. "
Use Case (CA) - âA use case describes application-specific business rules as opposed to the Critical Business Rules within the Entities.â
Domain-Driven Design Definitions
Entities (DDD) - Pretty much the same as CA.
Domain Services (DDD) - Domain services carry domain knowledge; application services donât (ideally). Domain services hold domain logic that doesnât naturally fit entities and value objects.
Introduce domain services when you see that some logic cannot be attributed to an entity/value object because that would break their isolation. - from Vladimir Khorikov's post on Application vs. Domain Services.
Application Services (DDD) - Every bounded context has several of these. They're used in order to retrieve the appropriate domain objects to pass to domain services.
I should do a better job of teaching by example on the blog, but more content on that is in the works.
Hopefully that explains my stance on why Application Services are most analogous to Use Cases from the Clean Architecture.
Continuing with your comment.
Use-cases do not belong to the application layer, the belong to the domain layer.
Going to have to disagree with that part again, just based on my own interpretation of DDD and CA.
If you'd dispatch DomainEvents in the domain layer, then you have one less layer that needs to know about DomainEvents.
I believe if we were to dispatch events from the Domain Layer, the Domain Layer would need to know if a transaction has completed in order to not only create the Domain Event, but to also dispatch it after the database transaction completes. It's the same reason that I stray from TypeORM, because the decorators break domain-layer encapsulation by requiring infrastructure layer configuration.
You can test whether your domain events get dispatched as expected, without having to implement the infrastructure layer (e.g. an ORM)
You can still do this without a database connection. Here's a rough example.
Shouldn't every domain implement it's own repository/ORM? One could make the point that it's not a good design decision for billing to dependent on the traderRepo, because if you're going to micro-services (split by domain), then you'd have to create a dependency between your billing domain and your trading domain.
You're raising an excellent concern here. One of the things we need to do when we graduate to microservices is to map out (perhaps w/ Context Maps) the relationships between bounded contexts. Notice I used the term bounded context here instead of subdomain? You might already know this, but bounded contexts are the actual physical separation of deployments.
If we did a really granular microservice deployment (billing, users, vinyl, etc were each their own bounded context), we'd need to establish a way for everyone to communicate with each other.
I've seen companies create server-side libraries for each microservice with Swagger-TypeScript gen, and that's whats used to communicate with other bounded contexts (microservices) throughout the enterprise.
Tons of different approaches here we could take. I'd like to spend more time examining options here.
But at the monolith level (like this), it's up to your discretion, but at least know when (potential future) boundaries are being crossed.
@Khalil Thank you for your response! I agree with your point, that use-cases are application services. Perhaps I should do some remodelling...
You mentioned the following: "I believe if we were to dispatch events from the Domain Layer, the Domain Layer would need to know if a transaction has completed in order to not only create the Domain Event, but to also dispatch it after the database transaction completes."
I also agree with this statement, becasue the domain layer should not know about the database. (Oftopic: this is a clear indication that I categorized use-cases incorrectly, as use-cases have access to the database. Hence, they don't belong to the domain layer).
Due to my missunderstanding about where the use-case should be positioned I made a misstake in my reasoning as to where to dispatch the domain event. I fire these events in my use-cases; thus in my application layer. Doesn't it make more sense to both fire and listen to events in the application layer? So that only the application layer and domain layer knows about domain events?
From a testing point of view, if you do this in the repository (which is certainly possible to test) you would need to have some form of an implementation of the repository before you can test whether events get dispatched correctly. If, on the otherhand, you'd fire domain events in the application layer, you only need to mock the repository interface.
In reply to: Here are my thoughts on some of the ideas and comments you raised (which are very constructive, btw).
I really like these kind of discussions! I hope others can benefit from this discussion as well. It certainly helped me.
I think either of those approaches could work quite well. One is a little more work to keep DRY though.
The only thing is that we don't want to have to manually write `DomainEvents.dispatchEventsForAggregate(id: UniqueEntityId)` at the end of every application layer use case.
To keep things DRY, we'd want to have that somewhere in the base abstract `UseCase` class. That somewhat complicates things and means that we have to 1) remember to set the current aggregate we're focused on, 2) dispatch aggregate events after the `execute` method has been called, but from the base class.
This is pushing us towards the Unit of Work pattern, which can be pretty challenging to implement manually.
Like everything in software- tradeoffs :)
Hey Khalil,
How do you maintain consistency between two aggregates?
Let's say we have an Order Aggregate and Approval Aggregate which are referenced to each other by Ids.
Though they are not atomic and need not be inserted in same transaction, we may need to maintain consistency between them. Say Whenever an order is placed, an approval should be sent and Vice-versa(whenever the approval is made, order should be updated). As i have decoupled these 2 logic using events, how would i maintain consistency.
Is there a pattern to manage transactions in eventual consistency?
Hey Vicky,
The answer to that is event handlers. In my projects, I call them Subscriptions, but I think they're more commonly known as Event Handlers.
We showed how to do this kind of thing in the article.
When the transaction on the Order aggregate completes and it dispatches all domain events on the marked Order aggregate.
Let's say the domain event is `OrderPlaced`.
That means that we'd want a subscriber/event handler to send an approval after the order was placed.
My naming scheme is to create a subscriber called `AfterOrderPlaced`. In it, I would invoke the `sendApproval` use case in response to the `OrderPlaced` event.
For several real-world examples of this kind of thing, check out the subscriptions/event handlers in DDDForum.com.
Hi Khalil, that Domain Events class; seems like a memory leak as thereâs no way to clean up the registered aggregates e.g whoâs domain events wonât be triggered eg due to failed transaction.
imo the main issue with that implementation is that itâs a static singleton.
Instead it should be a request scoped instance, which also means you can get rid of these clear methods useful for testing etc.
you could still have static methods that may try to access request scoped domain events registry through e.g continuation local storage or so.
my solution instead is to register the entities with the (request scoped) database context (upon ad for load), and on successful transaction iterate over these entities and extract and raise their domain events. The only thing is that the execution order may not be maintained across multiple entities, but I havenât ran into issues with that myself.
Good point. It'd be fun to try to fix that as pragmatically as possible. Your approach sounds interesting. Feel free to shoot me over some code! :)
Cool article, but won't this mean that you can't guarantee the order of execution for the subscribers?
Is there a particular reason youâre not using Nodeâs EventEmitter?
Thanks for sharing your knowledge!
No reason other than the fact that this approach was inspired by Udi Dahan's initial approach to this problem back in 2009. You could definitely get something like this working with EventEmitters as well.
I'm trying to implement this on a Email Service and it is very hard to implement because there are no hooks to dispatch the domain events
If that's the case, you'll want to roll your own kind of infrastructure to wrap every **transaction** with.
The Unit of Work pattern is a common approach, though it can be tricky to get working because it touches on just about everything.
I'll put it on my todo to create a trivial approach to decoupling business logic with events. Probably using Node's EventEmitter.
FWIW, if I'm not mistaken in TypeORM Event Listeners' and Event Subscribers' the "after" hooks are executed before the transaction commits (but after the individual Entities are saved).
Can you explain what happens if the subscriber fails to process the message?
Hi Khalil,
Love how you present this complex subject in such a consumable way.
However I still seem to have missed the part that explains how you handle usecases that depend on the successful outcome of multiple events.
I have the following scenario:
My current approach is to have an organisation domain entity Enrollment and have a afterEnrollmentCreated subscription for User, Organisation and Person.
Enrollment can subscribe to UserCreated to create Person.
The problem I'm facing is how to link the Person as being a member of a new Organisation. I can't use afterPersonCreated to create an organisation, because this is only true when enrollment takes place.
What am I missing?
Khalil, thanks for another informative post.
Most of the aforementioned events appear to fall into the âfire and forgetâ category. How should the code react when an event handler fails for an event whose handler behavior is critical to the outcome of the use case?
example: failure on sending a slack message notification may be OK. log the error, troubleshoot the connection, no problem. Future slack notifications are sent and received as expected. However, the User event to generate a unique name, a failure here seems like it could be more problematic. Envision a case where you actually wanted to prevent the User from being created if there was an issue handling this specific event. More concretely: The target User aggregate is modified by another process (e.g. profile updated directly by the account owner) after it is fetched/hydrated, but before persisting our copy containing the freshly auto generated name (but with the obsolete profile). Using something like optimistic concurrency, the save action (initiated by our own internal domain event) should fail or we risk overwriting the userâs profile update. How would we cascade this failure back to some place meaningful to roll back the entire aggregate? (User creation failed - internal error generating username e.g.). Are we sure that the username shouldnât be generated when the aggregate is first created? Why handle it later?
Hi Khalil,
When you inject sequelizeModels as any don't we loose type safety and auto completion?
It seems injecting all models looks unnecessary for the logic to be carried out.
Very good article.
To answer @Patrick Roza's comment about memory leak it's actually very easy to fix when using the repository pattern properly.
Just remove the `DomainEvents.markedAggregates` array and every reference to it. Then do something like this instead:
As every persistence operation are supposed to go through these create / update methods in the repository, this make dispatching events independent of the ORM.
DomainEvents does not need to keep a list of marked aggregates for dispatch anymore, which solves our memory leak problem.
Of course this means that you need to call DomainEvents.dispatchEventsForAggregate every time you persiste something. But this shouldn't be a problem with the repository pattern as most of the time you only to do it inside 2 methods: create & update.
Thank you for your articles.
Should we use value objects in Domain Events or should they be only plain JS objects? My BCs communicate via messaging infrastructure (AWS EventBridge) so all events are sent in JSON format. I donât think it would be a good idea to place serialize and deserialize methods on the events since this should not be a concern of the domain. However adding a sort of data mapper for each and every event would bloat the codebase and add complexity.
Do you have any thoughts/guidance regarding this?
Thank you,
Florian
Thank you for your awesome series on DDD and Clean Architecture, really appreciate.
My question: I have been building some kind of e-commerce app deployed as events based microservices, (I did not apply DDD or Clean Architecture so far). Though, I was using NATS message broker for sending/receiving events between microservices and they were being dispatched right in http handlers without waiting them resolved (async) {reason: not to keep users waiting extra time for something which is truly system's concern} whether it was successful or not but NATS was handling this, it supports at-least-once semantics.
Now, I was trying to adapt what you have been teaching to my project, so far so good except I am struggling with how to hook up NATS publishers to Aggregate's dispatch call. Can you please provide some guidelines on this matter if possible with practical examples
Thank you in advance!
Bug report:some var in the code in this article does not sync with real code in github, search "UserCreated" in this article 's code, many of them should be corrected to "UserCreatedEvent", this typo caused a lot of confusion.
Hey Khalil, thank you, really good post and has helped me hugely.
One question I have is how would you recommend to manage `correlationId` and `causationId` (or any other metadata) with this approach, given that both feel like they are application layer concerns, but are needed, for example, to drive Long Running Processes/ Process Managers and for observability purposes (to understand the flow of events throughout a system).
More specifically, my API controller adds a `correlationId` to each command, this is passed to each use case in the application layer, which in turn leverages the Domain Layer to perform the requested command against a given aggregate root. Previously I was emitting events in the application layer on completion (not good...!), but was able to then re-use the `correlationId` in any resulting events/ enabling continuity of the `correlationId` across further commands and events in the process - I can then filter logs by a given correlationId and see what's happened/ use persisted state to drive exception management etc.
I may be over thinking it, but would appreciate your perspective.