Domain Driven Design - Entity and Value objects

When I used to start a new Node.js project, I usually started with analyzing and modelling the RESTful API and possible the database schemas and models. This means I would mock the data and wire up som basic route handlers, controllers, for different RESTful endpoints to ultimately test them with Postman.

A small peek into how DDD can help you focus on the right thing.

In other words, I was building my app from the database, or the RESTFul API up and thinking CRUD first. If you have read the Clean Architecture, by Uncle Bob, you know you should never put business logic in the Controllers and that's exactly what this approach to starting lead to.

An example of a controller when using this approach could look like this.

class MovieController {
  private models
  constructor(models) {
    this.models = models
  }
  private notFound(response, message) {}
  private ok(response) {}
  
  public async rentMovie(request, response) {
    const { movieId, customerId } = request
    // ORM models
    const { Movie, Customer, RentedMovie, CustomerCharge } = this.models;

    // Get the raw orm records, as JSON
    const movie = await Movie.findOne({ movie_id: movieId });
    const customer = await Customer.findOne({ customer_id: customerId });

    // 401 error if not found
    if (!movie) {
      return this.notFound(response, 'Movie not found')
    }

    // 401 error if not found
    if (!customer) {
      return this.notFound(response, 'Customer not found')
    }

    // Create a record that a movie was rented
    await RentedMovie.create({ customer_id: customerId, movie_id: movieId });

    // Create a charge for this customer.
    await CustomerCharge.create({ amount: movie.rentPrice })
    
    return this.ok(response);
  }
}

This is quick approach and could get into trouble as soon changes in the business rules are required.

Business rules in CRUD first code

Let's say we need to apply the business rules; The customer should not be able to rent a video if they;

  • have rented the maximum amount of movies at one time,
  • have unpaid balances.

An implementation of this in the controller could then look like this.

class MovieController {
  private models
  constructor(models) {
    this.models = models
  }
  private notFound(response, message) {}
  private fail(response, message) {}
  private ok(response) {}

  public async rentMovie(request, response) {
    const { movieId, customerId } = request
    
    // We need to add another model - CustomerPayment 
    const { 
      Movie, 
      Customer, 
      RentedMovie, 
      CustomerCharge,
      CustomerPayment
    } = this.models;

    const movie = await Movie.findOne({ movie_id: movieId });
    const customer = await Customer.findOne({ customer_id: customerId });
    
    if (!movie) {
      return this.notFound(response, 'Movie not found')
    }
    
    if (!customer) {
      return this.notFound(response, 'Customer not found')
    }
    
    // Get the number of movies that this user has rented
    const rentedMovies = await RentedMovie.findAll({ customer_id: customerId });
    const numberRentedMovies = rentedMovies.length;

    // Enforce the rule
    if (numberRentedMovies >= 3) {
      return this.fail('Customer already has the maxiumum number of rented movies');
    }

    // Get all the charges and payments so that we can 
    // determine if the user still owes money
    const charges = await CustomerCharge.findAll({ customer_id: customerId });
    const payments = await CustomerPayment.findAll({ customer_id: customerId });

    const chargeDollars = charges.reduce((previousCharge, nextCharge) => {
      return previousCharge.amount + nextCharge.amount;
    });

    const paymentDollars = payments.reduce((previousPayment, nextPayment) => {
      return previousPayment.amount + nextPayment.amount;
    })

    // Enforce the second business rule
    if (chargeDollars > paymentDollars) {
      return this.fail('Customer has outstanding balance unpaid');
    }

    // Create a record that a movie was rented
    await RentedMovie.create({ customer_id: customerId, movie_id: movieId });

    // Create a charge for this customer.
    await CustomerCharge.create({ amount: movie.rentPrice })
    
    return this.ok(response);
  }
}

This solution works for CRUD design first approach, but there are drawbacks.

Encapsulation

If the complexity of our business model grows, we could move the domain logic to the service but, it is just to re-locate the same logic somewhere else and by doing so, we will risk ending up with an anemic domain model.

Wikipedia says an Anemic Domain Model "is the use of a software domain model where the domain objects

contain little or no business logic (validations, calculations, business rules etc.)"

In other words. When the classes that describe the models (movie and customer), and the classes that perform operations on the model are separate we enter the anemic model land. The services contain all the domain logic while the domain objects themselves contain practically none.

Anemic Domain Models are largely the cause of a lack of encapsulation and isolation.

Discoverability

When you look at a class, and it’s methods for the first time, it should accurately describe to you the capabilities and limitations of that class. When we co-locate the capabilities and rules of the Customer to an infrastructure concern (controllers), we lose some of that discoverability for what a Customer can do and when it’s allowed to do it.

CRUD design is a transaction script approach

According to Martin Fowler, the CRUD design approach sound to me that it is a transaction script approach.

See: Transaction Script

The case for DDD and domain model first

In DDD, we can from a declarative perspective, design the business rules that answer what and when something can be done which make it much more readable, flexible and expressive.

If we were to take our previous example and introduce some concepts from DDD, we are using Movie and Customer entity objects, containing business rules to perform magic and, in an even better architecture, we would let our controller inherit from a BaseController containing an abstract method which we can use to implement all logic in a use case object instead and let the controller only manage the request and response cycle.

// Entity objects that contain business rules
import { AppError } from '../../../../core/common/AppError'
import { Result, left, right } from '../../../../core/common/Result'
import { Movie, Customer } from '../entities'
import { UseCase } from '../../../../core/domain/UseCase'
import { RentMovieDTO } from './DeleteUserDTO'
import { RentMovieResponse } from './DeleteUserResponses'
import { RentMovieErrors } from './DeleteUserErrors'


class RentMovie implements UseCase<RentMovieDTO, Promise<RentMovieResponse>> {
  private movieRepo: IMovieRepo;
  private customerRepo: ICustomerRepo;
  constructor (movieRepo: IMovieRepo, customerRepo: ICustomerRepo) {
    this.movieRepo = movieRepo;
    this.customerRepo = customerRepo;
  }
  
  // Implementation of use case method inherit from abstract BaseController
  public async execute(rentMovieDto: rentMovieDTO, response): Promise<RentMovieResponse> {
    const { movieId, customerId } = rentMovieDto
    
    // Returns movie and costomer entity objects via mappers
    const movie: Movie = await movieRepo.findById(movieId);
    const customer: Customer = await customerRepo.findById(customerId);
    if (!movie) {
      return left(new RentMovieErrors.MovieNotFound()) as RentMovieResponse
    }

    if (!customer) {
      return left(new RentMovieErrors.CustomerNotFound()) as RentMovieResponse
    }

    //
    // The declarative magic happens here.
    //
    const rentMovieResult: Result<Customer> = customer.rentMovie(movie);
    if (rentMovieResult.isFailure) {
      return left(new RentMovieErrors.CustomerNotAllowed()) as RentMovieResponse
    }
    
    await customerRepo.save(customer)

    return right(Result.ok<void>())
  }
}

So what is the difference? The controller does not "implement" any business rules, instead, enforcing of the business rules are now implemented in the movie and customer entity objects. The movie and the customer objects are entity objects, created by the repository and mappers. That's why the method rentMovie, in the entity object customer, can perform it's magic, it knows about what and when it can rent a video.

When model the domain logic we want to express what it;

  • can do,
  • when it can do it, and
  • what conditions dictate when it can do that thing.

That also means we are enforcing model invariants, which is the same as ensuring the data integrity of an object and, by ensuring data integrity, we mean;

  • what shape is this data allowed to take,
  • what methods can be called and when,
  • what are the required parameters and pre-conditions in order to create this object.

A business object methods need to preserve the invariants of an object and to constrain the state stored in the object to protect the model from corruption.

Entity objects

In DDD, to model the domain logic, we are using entities (or value objects), and implements the business logic. Entity objects are different to value objects and are characterized by;

  • have a lifespan, a history of what happened to them and how they changed,
  • they are mutable, their internal state can change during their lifespan,
  • have their own intrinsic identity, which refers to a unique id, if data in two entity instances is the same (except for the id property), we don’t deem them as equivalent.

When coding in Typescript and Node.js, I have created an abstract Entity class that I inherit from when I model my domain entity classes that fulfill the characteristics of an entity object.

Here is my current implementation of the abstract Entity class.

// A class generating unique id
import { UniqueEntityID } from './UniqueEntityID'

/**
 * Entity type comparator
 * @param v
 */
const isEntity = (v: any): v is Entity<any> => {
  return v instanceof Entity
}

/**
 * Entity class implementation
 * Use Entity to entities to enforce model invariants
 * @class
 */
export abstract class Entity<T> {
  /**
   * The id of the entity and it's readonly since it
   * should not be able to change after instantiated
   */
  protected readonly _id: UniqueEntityID
  /**
   * The props of the entity class are stored in this.props
   * to leave to the subclass to decide getters and setters
   */
  public readonly props: T

  /**
   * Creates a new Entity instance
   * @param props
   * @param id
   */
  constructor(props: T, id?: UniqueEntityID) {
    this._id = id ? id : new UniqueEntityID()
    this.props = props
  }

  /**
   * Equal comparator based on Identifier equality
   * @param object
   */
  public equals(object?: Entity<T>): boolean {
    if (object == null) {
      return false
    }

    if (this === object) {
      return true
    }

    if (!isEntity(object)) {
      return false
    }

    return this._id.equals(object._id)
  }
}

Value objects

Value objects are more used in DDD, and their characteristics are;

  • they do not have a lifespan,
  • they can not live by their own, they should belong to an Entity object,
  • they can not be persisted on their own, they are persisted by their entities they belong to,
  • they do not have an id, they are equal by their attributes (value),
  • they are immutable, and thus representing a snapshot of the state.

Here is my current implementation of the abstract ValueObject class.

/**
 * Value object properties interface
 */
interface IValueObjectProps {
  [index: string]: any
}

/**
 * Abstract class implements value objects
 * @class
 */
export abstract class ValueObject<T extends IValueObjectProps> {
  /**
   * The props of the value object are stored in this.props
   * to leave to the subclass to decide getters
   */
  public props: T

  /**
   * Creates a new ValueObject instance
   * @param props
   */
  protected constructor(props: T) {
    this.props = {
      ...props,
    }
  }

  /**
   * Equality comparator for value objects
   * @param vo
   */
  public equals(vo?: ValueObject<T>): boolean {
    if (vo === null || vo === undefined) {
      return false
    }

    if (vo.props === undefined) {
      return false
    }

    return JSON.stringify(this.props) === JSON.stringify(vo.props)
  }
}

How to recognize a value object in your domain model?

It's not easy at first to know if a business object should be implemented as an Entity or, as a Value object, but the notion of an identity is helpful and could be a first guideline.

Ask yourself if you can safely replace an instance class with another with the same attributes, if so,

it should probably be a value object.

Summary

Domain-Driven Design is an approach to software development that centers the development on programming a domain model that has a rich understanding of the processes and rules of a domain. It is a natural starting point for any project to start to evaluate the business logic. You focus from the get going on what should be done, when it should be done and so on instead of focus on infrastructure concerns.

I have tried to describe that separation of concerns, single responsibility principles and, a clean architecture really matters and are important even if you need to get something up and running quickly, you want to respond to prototype apps, you are working on a small app, etc.

I wrote in my previous article about the importance of the Ubiquitous Language and its role to understand and evaluate the business model. This language should be based on the Domain Model used in the software - hence the need for it to be rigorous, since software doesn't cope well with ambiguity.

In a future article I will cover another important aspect of DDD, aggregate, entity and value objects in more detail, describing how they can relate to each other and help us model business requirements in a domain.

References:


Published: 2021-02-23
Author: Henrik Grönvall
Henrik Grönvall
Copyright © 2022 Henrik Grönvall Consulting AB