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.
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.
Let's say we need to apply the business rules; The customer should not be able to rent a video if they;
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.
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.
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.
According to Martin Fowler, the CRUD design approach sound to me that it is a transaction script approach.
See: Transaction Script
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;
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;
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.
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;
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 are more used in DDD, and their characteristics are;
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)
}
}
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.
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: