My TypeScript Software Design & Architecture book just prelaunched! Check out solidbook.io.
Close

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

This article is part of the upcoming DDD + TypeScript course. Check it.
Sep 26th, 2019 / 22 min read / Share / Edit on GitHub
In this article, we'll walk through the process of using Domain Events to clean up how we decouple complex domain logic across the subdomains of our application.

This is part of the Domain-Driven Design w/ TypeScript & Node.js 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?

UsersService.ts
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 to Users. Sending emails and slack messages most likely should belong to the Notifications subdomain, while hooking up marketing campaigns using a tool like Mailchimp would make more sense to belong to a Marketing subdomain. Currently, we've coupled all of the unrelated side-effects of createUser to the UsersService. 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 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.

UsersService.ts
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.

IDomainEvent.ts
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.

modules/users/domain/events/UserCreated.ts
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.

modules/users/domain/user.ts
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 a Vinyl 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.

core/domain/AggregateRoot.ts
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:

  1. add that domain event to a list of events that this aggregate has seen so far, and
  2. we tell something called DomainEvents to mark this 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.

core/domain/events/DomainEvents.ts
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.

modules/notification/subscribers/AfterUserCreated.ts
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.

modules/users/useCases/createUser/CreateUserUseCase.ts
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.

infra/sequelize/hooks/index.ts
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'));

})();

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 👨‍🎤.


6 Comments

Submit
Abel
2 months ago

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?

Khalil Stemmler
2 months ago

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.



Vignesh
2 months ago

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)

Peter
2 months ago

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:


  1. The domain layer knows about domain events. Use-cases do not belong to the application layer, the belong to the domain layer. So, in this design, your aggregate roots needs to know about DomainEvents (domain layer) + your ORM needs to know about DomainEvents (infrastructure layer) + your application layer needs to know about DomainEvents (because this is where you should listen to events). If you'd dispatch DomainEvents in the domain layer, then you have one less layer that needs to know about DomainEvents.
  2. You can test whether your domain events get dispatched as expected, without having to implement the infrastructure layer (e.g. an ORM). You can write unit test on the use-case, and check whether the DomainEvents dispatcher is called.


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.

Khalil Stemmler
2 months ago

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.

Khalil Stemmler
2 months ago

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.

Peter
2 months ago

@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.


Khalil Stemmler
a month ago

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 :)

Vicky
a month ago

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?

Khalil Stemmler
a month ago

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.



Patrick Roza
a month ago

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.

Khalil Stemmler
a month ago

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! :)


Stay in touch!



About the author

Khalil Stemmler,
Developer Advocate @ Apollo GraphQL ⚡

Khalil is a software developer, writer, and musician. He frequently publishes articles about Domain-Driven Design, software design and Advanced TypeScript & Node.js best practices for large-scale applications.



View more in Domain-Driven Design



You may also enjoy...

A few more related articles

How to Handle Updates on Aggregates - Domain-Driven Design w/ TypeScript
Nov 18th, 2019 / 20 min read
In this article, you'll learn approaches for handling aggregates on Aggregates in Domain-Driven Design.
How to Design & Persist Aggregates - Domain-Driven Design w/ TypeScript
Jul 24th, 2019 / 29 min read
In this article, you'll learn how identify the aggregate root and encapsulate a boundary around related entities. You'll also lear...
Does DDD Belong on the Frontend? - Domain-Driven Design w/ TypeScript
Sep 24th, 2019 / 18 min read
Should we utilize Domain-Driven Design principles and patterns in front-end applications? How far does domain modeling reach from ...
An Introduction to Domain-Driven Design - DDD w/ TypeScript
Jul 30th, 2019 / 13 min read
Domain-Driven Design is the approach to software development which enables us to translate complex problem domains into rich, expr...

Want to be notified when new content comes out?

Join 3000+ other developers learning about Domain-Driven Design and Enterprise Node.js.

I won't spam ya. 🖖 Unsubscribe anytime.

Get updates