No special messages at the moment. Just have a good day and stay hydrated!

Where Do Domain Events Get Created? | Domain Driven Design w/ TypeScript

Jul 4th, 2019 / 5 min read / Share / Edit on GitHub
In this post, I explain how good ol' fashioned encapsulation + TypeScript getters and setters enable us to effectively create Domain Events directly from the aggregate roots in our domain-driven design projects.

Last time, we talked about how to create a facade over our ORM using the Repository pattern. Today, we're talking a little bit about where we create Domain Events.

Domain Events are a huge part of Domain-Driven Design.

When implemented correctly, they allow us to chain domain logic, maintain single responsibility, and cross architectural boundaries (subdomains) using the Observer pattern (which is a well-known way to separate concerns and decouple async logic).

The question is: "where do Domain Events get created"?

Domain Events are a part of the domain layer, so they belong with all the other domain layer constructs like entities, value objects and domain services.

OK cool, but where exactly do they get created?

Right on the aggregate root.

Allow me to explain.

To me, the most beautiful thing about Domain-Driven Design is that it urges you towards creating your own domain-specific language (DSL).

You expose only the methods and attributes that make sense to the domain. And those methods and attributes only belong on entities and value objects that it makes sense to belong on.

class User {
  // factory method to create users, yep
  public static create (props): User { }  

  // yup, also a valid domain operation
  public deactivate (): void {}       
  // ❌ No. A client shouldn't need to use this operation.
  // Use a Value Object to encapsulate validation logic instead: 
  public validateFirstName (firstName): boolean {}            

Determining what makes sense is hard. It usually takes a lot of conversation with domain experts and multiple iterations of the model.

This increased importance on YAGNI, and only exposing operations that are valid to the model are also a big reason why getters and setters get a lot of usage for us in Domain-Driven Design.

Using getters and setters, not only do we get to specify exactly what is allowed to be accessed/changed, but we also get to specify when it's allowed to be accessed/changed, and what happens when it's accessed/changed.

When key properties are accessed or changed, it might make sense to create a Domain Event.

For example, if we were working on White Label's feature that enables Traders to accept or decline Offers, we'd have to walk through the process of determining where Domain Logic should belong.

Following that process, we'd discover that acceptOffer() and declineOffer() perform mutations to the Offer aggregate root itself.

Those operations can be done without the involvement of any other domain entities, so it makes sense to locate them directly on the Offer aggregate root.

export type OfferState = 'initial' | 'accepted'  | 'declined'

interface OfferProps {
  state: OfferState;

export class Offer extends AggregateRoot<OfferProps> {

  get offerId (): OfferId {
    return OfferId.create(this._id);

  get offerState (): OfferState {
    return this.props.state;

  public acceptOffer (): Result<any> {
    switch (this.offerState) {
      case 'initial':
        // Notice how there is not a public setter for the
        // 'state' attribute. That's because it's important that
        // we intercept changes to state so that we can create and add
        // a Domain Event to the "observable subject" when it's
        // appropriate to do so.
        this.props.state = 'accepted';
        // And then we create the domain event.
        this.addDomainEvent(new OfferAcceptedEvent(this.offerId));
        return Result.ok<any>();
      case 'accepted':
        return<any>(new Error('Already accepted this offer'));
      case 'declined':
        return<any>(new Error("Can't accept an offer already declined"));
        return<any>(new Error("Offer was in an invalid state"));

  public declineOffer (): Result<any> {
    switch (this.offerState) {
      case 'initial':
        // Same deal is going on here.
        this.props.state = 'declined';
        this.addDomainEvent(new OfferDeclinedEvent(this.offerId));
        return Result.ok<any>();
      case 'accepted':
        return<any>(new Error('Already accepted this offer'));
      case 'declined':
        return<any>(new Error("Can't decline an offer already declined"));
        return<any>(new Error("Offer was in an invalid state"));

  private constructor (props: OfferProps, id?: UniqueEntityId) {
    super(props, id);

And if we were taking the use case approach of hooking this up, executing this feature would look like:

export class AcceptOfferUseCase implements UseCase<AcceptOfferDTO, Result<Offer>> {
  private offerRepo: IOfferRepo;
  private artistRepo: IArtistRepo;

  constructor (offerRepo: IOfferRepo) {
    this.offerRepo = offerRepo

  public async execute (request: AcceptOfferDTO): Promise<Result<Offer>> {
    const { offerId } = request;
    const offer = this.offerRepo.findById(offerId);

    if (!!offer === false) {

    // Creates the domain event

    // Persists the offer and dispatches all created domain events;
    return Result.ok<Offer>(offer)

Because Domain Events are part of the domain, we'll always want to try to place Domain Events as close to the entities/aggregate roots as possible.

If we can trust that the domain layer will always generate the appropriate domain events in those key scenarios, we can enable several applications (perhaps deployed in separate bounded contexts and propogated over the network using a message queue like RabbitMQ) to implement their own varying application layer logic to handle these events however they please.

For example, if our Billing subdomain were to subscribe to the OfferAcceptedEvent, we might want to fulfill the trade by facilitating the trade between the two parties and charging a 0.2% processing fee.



I hope this article was useful to you! Consider checking out my sponsors. I can continue to write quality articles for free because of them.


Thoughts? Share the article if you think it'll be useful to someone + join the discussion about this post on Twitter!

Stay in touch!

About the author

Khalil Stemmler

Khalil Stemmler is a Developer / Designer and co-founder of Univjobs. He frequently publishes articles about Domain-Driven Design and Advanced TypeScript & Node.js best practices for large-scale applications.

You may also enjoy...

A few more related articles

Implementing DTOs, Mappers & the Repository Pattern using the Sequelize ORM [with Examples] - DDD w/ TypeScript
Jun 20th, 2019 / 14 min read
There are several patterns that we can utilize in order to handle data access concerns in Domain-Driven Design. In this article, w...
Understanding Domain Entities [with Examples] - DDD w/ TypeScript
May 28th, 2019 / 12 min read
Entities are the first natural place we should aim to place business logic in domain-driven applications. In this article, we talk...
REST-first design is Imperative, DDD is Declarative [Comparison] - DDD w/ TypeScript
May 15th, 2019 / 11 min read
A comparison between designing Node.js applications using REST-first design and Domain-Driven Design.
An Introduction to Domain-Driven Design - DDD w/ TypeScript
Apr 9th, 2019 / 10 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 2000+ other developers learning about Domain-Driven Design and Enterprise Node.js.

I won't spam ya. 🖖 Unsubscribe anytime.

Get updates