Domain Driven Design - The Domain Layer

In the last article I described the implementation differences between entity objects and value objects where the most significant difference is that entity objects have a lifespan are mutable and have their own intrinsic identity based on a unique id. Value objects on the other hand, have no lifespan, belongs to entity objects, have no id, are equal by their attributes, are immutable and thus represent a snapshot of state. I also mentioned that these software artifacts is used in the domain model and implement the business requirements defined by the UbiquitousLanguage.

How do we define a domain model?

Domain-Driven Design is about designing software based on models of the underlying domain. A model acts as a UbiquitousLanguage to help communication between software developers and domain experts. It also acts as the conceptual foundation for the design of the software itself - how it's broken down into objects or functions. To be effective, a model needs to be unified - that is to be internally consistent so that there are no contradictions within it.

Modelling a large domain can get hard quickly and building a single unified model is more difficult the bigger organization. More people or groups of people leads to subtle different vocabularies and different interpretation in different parts of an organization and modelling often end up in confusion.

Domain-Driven Design advocates through the strategic concept to divide up large systems into bounded contexts, where each can have a unified model.

Bounded contexts have both unrelated concepts but also share concepts. Different contexts may have completely different models of common concepts with mechanisms to map between these uncertain concepts for integration.

To define a domain model it's therefore important to explicitly define the context within which a model applies. In the Domain-Driven Design - the Context Map is designated as the primary tool used to make context boundaries explicit.

Software artifacts in domain models

I have already mentioned entity objects and value objects as software artifacts commonly used in the domain layer and will now touch other artifacts such as aggregates, domain services, and domain events. All elements in the domain layer are only dependent on each other and actively avoid dependencies any outer layers.

Architecture

Aggregates

Aggregate is a pattern in Domain-Driven Design. An aggregate is a cluster of domain objects that can be treated as a single unit and will have one of its component objects be the aggregate root. Any references from outside the aggregate should only go to the aggregate root. The root can thus ensure the integrity of the aggregate as a whole.

Aggregates are the basic element of transfer of data storage - you request to load or save whole aggregates. Transactions should not cross aggregate boundaries.

Domain Events

A domain event is, something that happened in the domain that you want other parts of the same domain to be aware of. The notified parts usually react somehow to the events. An important benefit of domain events is that side effects can be expressed explicitly.

The essence of a domain event is that you use it to capture things that can trigger a change to the state of the application you are developing.

Domain Service

The domain service consist of methods that don’t really fit on a single entity or require access to the application services. The domain service can also contain domain logic of its own and is as much part of the domain model as entities and value objects.

Register User - a use case to put it all together

It's time for some code examples and when I have played with Domain-Driven Design concepts, in Typescript, I have focused on user account as a domain or bounded context. Use cases is in my opinion a great method to model and designing software. The challenge has been to map it into the software artifacts of Domain-Driven Design.

Business requirements

The requirements of the use case "Register User" can be written like this.

Objective

The purpose of the use case is to allow anyone to register for the application and when done an email verification should be sent to the registered email address with a verification token in a link for the user to verify the registration.

The business rules are

A user who register to the application must enter;

  • a username: must be at least 5 chars long and not longer than 15 chars,
  • a password: must be 6 to 20 characters and at least one numeric, one uppercase and one lowercase,
  • an email address: must be a valid email address form

The user to be created must setting default values for other properties such as,

  • scope = profile,
  • isEmailVerified = false,
  • isAdmin = false,
  • isDeleted = false.

There must NOT be possible to overwrite an existing user.

  • A user marked for deletion is still existing.
  • A user can only have one username and one email address.

External boundaries

When the user account is created, the email verification services must be informed in order to send out a verification email. The email verification system will in a soon future be replaced with a message infrastructure.

Register User - implementing the domain model

Time to focus on the implementation of the domain model derived from the business requirements expressed in the use case and, using Domain-Driven Design software artifacts.

In our use case, the aggregate root for the domain is the User object. The interface of the User will be expressed as a Typescript interface and have the following shape;

export interface IUserProps {
  username: UserName // Value object
  email: UserEmail  // Value object
  credential?: UserCredential // Value object
  scope?: UserScope
  isEmailVerified?: boolean
  isAdminUser?: boolean
  isDeleted?: boolean
}

We know that username, password and email will be received as inputs to the use case and, they will be implemented as value objects since they belong to the User object, the rest of the parameters will be set with default values when creating the User object. The implementation of the value object UserName can be seen below;

import { ValueObject } from '../../../core/domain/ValueObject'
import { Result } from '../../../core/common/Result'
import { Guard } from '../../../core/common/Guard'

export interface IUserNameProps {
  username: string
}
export class UserName extends ValueObject<IUserNameProps> {
  // Validation rules
  public static maxLength: number = 15
  public static minLength: number = 5
  
  private constructor(props: IUserNameProps) {
    super(props)
  }
  // getter for the value
  get value(): string {
    return this.props.username
  }
  /**
   * Factory method to create an instance and apply the validation rules
   * @param username The user name
   */
  public static create(username: string): Result<UserName> {
    const minLengthResult = Guard.againstAtLeast(this.minLength, username, 'username')
    if (!minLengthResult.isSuccess) {
      return Result.fail<UserName>(minLengthResult.message)
    }
    const maxLengthResult = Guard.againstAtMost(this.maxLength, username, 'username')
    if (!maxLengthResult.isSuccess) {
      return Result.fail<UserName>(maxLengthResult.message)
    }
    return Result.ok<UserName>(new UserName({username: username}))
  }
}

Q) Why do I not inject the creation of the UserName object to the User object and perform the validation there?

A) Single responsibility rule: validation rules belong to UserName and not User.

Q) Why do I implement a factory method and using a private constructor to create a value object?

A) I have chosen this approach in order to enforce object creation rules, when using the "new" keyword to create objects there is not in my view an elegant way to enforce validation rules. Throwing errors in the constructor can be done but is not elegant and sometimes the creation of the stored value (expressed as props) may need an async function and that is not possible/recommended. For example, I do not store the plain text of the password in the UserCredential value object - instead I store the salted and hashed password. Salt and hash a password should in Node.js be implemented as an async function.

The other objects - UserCredential and UserEmail - follows the same implementation design and, I do not provide code examples, they do provide a public static factory method and uses private constructors as well.

As you can see, this way we can implement the business requirements from the use case description;

  • a username, must be at least 5 chars long and not longer than 15 chars,
  • a password, must be 6 to 20 characters and at least one numeric, one uppercase and one lowercase,
  • an email address, must be a valid email address form

Now we head over to the implementation of the User object;

import { Result } from '../../../core/common/Result'
import { AggregateRoot } from '../../../core/domain/AggregateRoot'
import { UniqueEntityID } from '../../../core/domain/UniqueEntityID'

import { UserDomainEvent} from './events/UserDomainEvent'
import { UserName } from './userName'
import { UserEmail } from './userEmail'
import { UserCredential } from './UserCredential'
import { UserScope } from './userScope'
import { UserId } from './UserId'

export class User extends AggregateRoot<IUserProps> {
  private constructor(props: IUserProps, id?: UniqueEntityID) {
    super(props, id)
  }
  
  get id(): UniqueEntityID {
    return this._id
  }
  
  get username(): UserName {
    return this.props.username
  }
  
  get email(): UserEmail {
    return this.props.email
  }
  
  get credential(): UserCredential {
    return <UserCredential>this.props.credential
  }
  
  get isEmailVerified(): boolean | undefined {
    return this.props.isEmailVerified
  }
  
  set isEmailVerified(value) {
    this.props.isEmailVerified = value
    // Create a domain event if email has been verified
    const userEmailverified = new UserDomainEvent(this.id, 'META: Changed isEmailVerified')
    this.addDomainEvent(userEmailverified)
  }
  
  get isAdminUser(): boolean | undefined {
    return this.props.isAdminUser
  }
  
  get isDeleted(): boolean | undefined {
    return this.props.isDeleted
  }

  /**
   * Factory method creating the User entity
   * @param {IUserProps} props
   * @param {UniqueEntityID} id
   * @return {Result<User>}
   */
  public static create(props: IUserProps, id?: UniqueEntityID): Result<User> {
    // Creates a new User object and sets default values
    const user = new User(
      {
        ...props,
        scope: props.scope ? props.scope : UserScope.create({ value: 'profile' }).getValue(),
        isDeleted: props.isDeleted ? props.isDeleted : false,
        isEmailVerified: props.isEmailVerified ? props.isEmailVerified : false,
        isAdminUser: props.isAdminUser ? props.isAdminUser : false,
      },
      id
    )
    // if a new user entity was created - create a domain event
    if(!id) {
      const userCreated = new UserDomainEvent(user.id, 'META: new user created')
      user.addDomainEvent(userCreated)
    }
    return Result.ok<User>(user)
  }
}

Q) Why does the User inherits from AggregateRoot?

A) The AggregateRoot implements logic for the domain service to handle domain events. Our User entity object now inherits from the AggregateRoot and thus have access to creating, dispatching and deleting domain events and as you can se we have implemented to create a new domain event when a new User object is created. The logic to detect if we are creating a new User is based on the id parameter passed into the factory function. If we do not get an id it is a new user, we would assume if we get an id it comes from an existing User object, that we have loaded in from a database.

You can also note we are setting default values to the other properties when creating the User object. Just as requirements told us.

If you are curious about the implementation of the abstract AggregateRoot class and, the DomainEvent class, here they are:

import { Entity } from './Entity'
import { IDomainEvent } from './events/IDomainEvent'
import { DomainEvents } from './events/DomainEvents'
import { UniqueEntityID } from './UniqueEntityID'

export abstract class AggregateRoot<T> extends Entity<T> {
  protected _domainEvents: IDomainEvent[] = []
  get id(): UniqueEntityID {
    return this._id
  }
  get domainEvents(): IDomainEvent[] {
    return this._domainEvents
  }
  public addDomainEvent(domainEvent: IDomainEvent): void {
    this._domainEvents.push(domainEvent)
    DomainEvents.markAggregateForDispatch(this)
  }
  public dispatchDomainEvents() {
    DomainEvents.dispatchEventsForAggregate(this.id)
    this.clearEvents()
  }
  public clearEvents(): void {
    this._domainEvents.splice(0, this._domainEvents.length)
  }
}

import { IDomainEvent } from '../../../../core/domain/events/IDomainEvent'
import { UniqueEntityID } from '../../../../core/domain/UniqueEntityID'

export class UserDomainEvent implements IDomainEvent {
  public dateTimeOccurred: Date
  public meta: any
  public aggregateId: UniqueEntityID
  public eventType: string = 'UserDomainEvent'

  constructor(aggregateId:UniqueEntityID , meta: any, type: string) {
    this.dateTimeOccurred = new Date()
    this.aggregateId = aggregateId
    this.meta = meta
    this.eventType = type
 }
}

The aggregate root inherits from the abstract class Entity and, the User object is extended to contain the methods for domain events from the AggregateRoot.

So far we have implemented the domain logic to the use case of register a user based on the business requirements but what about the use case implementation?

Register User - implementing the use case

Now we are thinking about the application layer in Domain-Driven Design where use cases belong. If we compare Uncle Bobs Clean Architecture with Domain-Driven Design, the application layer in Domain-Driven Design roughly correspond to the application business rules layer in Clean Architecture, which implements use case or user stories. The Domain-Driven Design terminology is trying to make a distinction between application services which depend on the domain layer, and domain services, which take part in implementing the domain logic.

Architecture

There is so many layers I manage to write in one article though. I will go through the use case implementation in more details in the next article but below you can se the code for my implementation of the "create user" use case.

As your already can see in the code, more software artifacts shows up and needs a detailed explanation. I will try to describe, Repository, DTO, Mappers and the use case associated Response type and how they fit into the application layer of Domain-Driven Design.

Example code of the use case implementation:

import { AppError } from '../../../../core/common/AppError'
import { Result, left, right } from '../../../../core/common/Result'

import { DomainEvents } from '../../../../core/domain/events/DomainEvents'
import { UseCase } from '../../../../core/domain/UseCase'

import { IUserRepo } from '../../repos/userRepo'
import { ICreateUserDTO } from './CreateUserDTO'
import { CreateUserErrors } from './CreateUserErrors'
import { CreateUserResponse } from './CreateUserResponse'

import { User } from '../../domain/User'
import { UserName } from '../../domain/userName'
import { UserEmail } from '../../domain/userEmail'
import { UserCredential } from '../../domain/UserCredential'
import { UserDomainEvent } from '../../domain/events/UserDomainEvent'

/**
 * Implements use case logic
 * @class
 */
export class CreateUser implements UseCase<ICreateUserDTO, Promise<CreateUserResponse>> {
  private userRepo: IUserRepo

  constructor(userRepo: IUserRepo) {
    this.userRepo = userRepo
  }

  /**
   * Implementation of the use case
   * @param createUserDTO
   */
  public async executeCommand(createUserDTO: ICreateUserDTO): Promise<CreateUserResponse> {
    let user: User
    let username: UserName
    let credential: UserCredential
    let email: UserEmail

    try {

      // Create value objects from DTO
      const isValidUserName = UserName.create(createUserDTO.username)
      const isValidUserCredential = await UserCredential.create(createUserDTO.password)
      const isValidUserEmail = UserEmail.create(createUserDTO.email)
      const combinedResult = Result.combine([isValidUserName, isValidUserCredential, isValidUserEmail])
      if (!combinedResult.isSuccess) {
        return left(new CreateUserErrors.ValidationError(combinedResult.error)) as CreateUserResponse
      }

      // The value objects where ok - get them
      username = isValidUserName.getValue()
      credential = isValidUserCredential.getValue()
      email = isValidUserEmail.getValue()

      // Check if user already exist and if so validate according to our business rules
      // Pay attention to that we are searching for an existing user with username and email
      // combined in a unique index in the database.
      let foundUser: User = <User>await this.userRepo.exists(username, email)
      if (foundUser) {
        
        // The user existed but was marked for deletion
        if (foundUser.isDeleted) {
          return left(new CreateUserErrors.UserIsMarkedForDeletion()) as CreateUserResponse
        }
        
        // The user existed but the username is already taken
        if (foundUser.username.equals(username)) {
          return left(new CreateUserErrors.UsernameTaken(username.value)) as CreateUserResponse
        }

        // The user existed but the email address existed
        if (foundUser.email.equals(email)) {
          return left(new CreateUserErrors.EmailAlreadyExists(email.value)) as CreateUserResponse
        }
      }

      // The User does not exists - create a User object
      // We know as well the factory method of the User object creates a domain event
      const isValidUser = User.create({ username, email, credential })
      if (isValidUser.isFailure) {
        return left(Result.fail<User>(isValidUser.error.toString())) as CreateUserResponse
      }

      // The User object is valid and contains consisted data and a domain event.
      // The user object is an aggregate root and only aggregate roots are allowed to
      // contain domain events. Domain events should never cross the transaction boundary 
      user = isValidUser.getValue()

      // Save the user
      // We are passing an aggregate root to the repository
      const isSaved = await this.userRepo.save(user)

      // If user where saved properly - dispatch all events for the aggregate root
      if (isSaved) {
        DomainEvents.dispatchEventsForAggregate(user.id)
      } else {
        // We could clear the created domain event, but we may also create 
        // another domain even informing that an attempt to create a user
        // failed
        user.clearDomainEvent()
      }

      // Return void since the use case is a command
      return right(Result.ok<void>())
    } catch (err) {
      return left(new AppError.UnexpectedError(err)) as CreateUserResponse
    }
  }
}

Summary

In this article I have continue put a focus on the ubiquitous language and how important it is to be able to model business requirements with a focus on bounded context in order to design great unified domain models.

I have described how to implement the domain layer using common DDD software artifacts based on business requirements expressed in use case format.

I have touched how to use tools such as "use cases or user stories" to cover business requirements and mentioned context mapping as another tool to use.

I have also shared my experimental code and hope you can be merciful for any obvious bugs or mistakes.

Bear in mind I'm doing this on my spare time, and unfortunately I have plenty of it and doing it all alone with no other to brainstorm with, which is not satisfying.

I love it though and apart from learning DDD I'm also learning Typescript and refreshing my mental thinking.

However, If you think I can add value to your business, do not hesitate to contact me. I would love communicating with other peers and solve real problems.

Happy coding. :-)


Published: 2021-03-01
Author: Henrik Grönvall
Henrik Grönvall
Copyright © 2022 Henrik Grönvall Consulting AB