Clean architecture with TypeScript: DDD, Onion
Sep 29, 2019
André Bazaglia
8 minute read

Introduction

During my 6+ years of professional experience, I had the opportunity of working in high tech companies that focus on efficient deliveries and take some concepts like availability and code quality very seriously. I had to deal with critical situations where bugs or system outages are inaceptable even if the impact is just a few seconds.

The focus of this article is not to cover big topics like DDD and Onion Architecture, but to provide an example of how to implement these two patterns in TypeScript. While the project used for this example is just an introduction point, you might feel comfortable for enhancing it introducing other concepts inside this architecture such as CQRS.

Why DDD?

You should write software that implements exactly the business requirements.

It makes it easy to test the domain layer, which means you can ensure all of your business rules are being respected and write long term bug-proof code. DDD together with Onion (covered on next topic) are a consistent way to avoid the cascade effect code, where you change one piece and the side effects are innumerable.

Why Onion?

It plays well with DDD, as everything is built on top of a domain model, which is the first circle in the picture, the most inner one. There are no direct dependencies on external layers in internal ones, just by injection. Plus it is super flexible, as layers have its own responsibilities and input data are validated by each layer according to its needs, meaning inner layers will always receive valid input from outer layers. Not to mention testability: unit tests are way easier as you can use interfaces to provide to your classes mocked dependencies to not rely on external parts of the system such as databases when testing.

Image of Onion Architecture

In TypeScript/Javascript, Inversion of Control (by Dependency Injection) means we are injecting/passing things as params instead of importing. In the following code examples, we are gonna use a library called Inversify, that allows us to declare dependencies using decorators to create classes that later can have dynamically created containers for resolving its dependencies.

The architecture

For this sample application…

We’re building a simple shopping cart application, where there are products (items) which can be added to a shopping cart. The cart may have some business rules, like minimum/maximum quantity of a unique item.

Let’s dive into the layers of the application now, starting from the inner and going to the outer ones, in sequence.

Domain

The domain layer is basically where all the business rules live. It doesn’t know any other layer, therefore, has no dependencies, and is the easier to test. Even if you change all your application around, the domain is still intact, as all it contains are business rules that anyone can read to understand what is desired by your application. It might also be where you want to start testing.

What we have here is a base abstract class called Entity, and the reason for that is you can have some logic in the Entity that is common to all the domain classes. In this example, the common logic I have is generating a collision-resistant ID optimized for horizontal scaling, which mean for all my entities I will use the same strategy.

export abstract class Entity<T> {
  protected readonly _id: string
  protected props: T

  constructor(props: T, id?: string) {
    this._id = id ? id : UniqueEntityID()
    this.props = props
  }

  // other common methods here...
}

With the Entity class already defined in our codebase, we’re ready to create our domain class, which is extending the abstract class Entity.

There’s nothing too complex on this class, but there are some interesting points you should give a special look at.

First, the constructor is private, which means new Cart() would fail, and this is the expected behavior. Again, as we are doing DDD, it is a nice practice to keep the domain class always at a valid state. Instead of directly instantiating an empty Cart object, we are using the Factory design pattern that returns an instance of the Cart class. Some validation could be made to ensure the creation is receiving all the required attributes. Similarly, we have getters and setters to provide all the interactions with the domain, and this is the reason why the class internal attributes/state is in an object (called props in this example), and the getters exposes the attributes that need to be public, as well as the public setters and methods that enable us to operates on the domain, guaranteeing its internal properties to always change to a valid state.

After all, the most important thing you should realize here is: The domain responsibility is to focus on behaviors, not properties.

export class Cart extends Entity<ICartProps> {
  private constructor({ id, ...data }: ICartProps) {
    super(data, id)
  }

  public static create(props: ICartProps): Cart {
    const instance = new Cart(props)
    return instance
  }

  public unmarshal(): UnmarshalledCart {
    return {
      id: this.id,
      products: this.products.map(product => ({
        item: product.item.unmarshal(),
        quantity: product.quantity
      })),
      totalPrice: this.totalPrice
    }
  }

  private static validQuantity(quantity: number) {
    return quantity >= 1 && quantity <= 1000
  }

  private setProducts(products: CartItem[]) {
    this.props.products = products
  }

  get id(): string {
    return this._id
  }

  get products(): CartItem[] {
    return this.props.products
  }

  get totalPrice(): number {
    const cartSum = (acc: number, product: CartItem) => {
      return acc + product.item.price * product.quantity
    }

    return this.products.reduce(sum, 0)
  }

  public add(item: Item, quantity: number) {
    if (!Cart.validQuantity(quantity)) {
      throw new ValidationError(
        'SKU needs to have a quantity between 1 and 1000'
      )
    }

    const index = this.products.findIndex(
      product => product.item.sku === item.sku
    )

    if (index > -1) {
      const product = {
        ...this.products[index],
        quantity: this.products[index].quantity + quantity
      }

      if (!Cart.validQuantity(product.quantity)) {
        throw new ValidationError('SKU exceeded allowed quantity')
      }

      const products = [
        ...this.products.slice(0, index),
        product,
        ...this.products.slice(index + 1)
      ]

      return this.setProducts(products)
    }

    const products = [...this.products, { item, quantity }]
    this.setProducts(products)
  }

  public remove(itemId: string) {
    const products = this.products.filter(product => product.item.id !== itemId)
    this.setProducts(products)
    this.emitCartMutation()
  }

  public empty() {
    this.setProducts([])
  }
}

While our Cart object can be operated using the domain methods across the entire application, eventually we might need to unmarshal it to a plain object: where we save it on the database or want to return a JSON response to a client. This can be easily achieved calling the method unmarshall().

Thinking of flexibility for elegant architectures, the domain layer is also where domain events are triggered. Event Sourcing might be implemented here, with events being emitted when the domain entity changes.

Use cases

Mainly we will compose our domain methods here, and eventually, use what was injected from the infrastructure layer to persist data.

We’re using a library called inversify for enabling Inversion of Control pattern, that is injecting a repository from infrastructure layer into this use case, enabling us to call the domain method to manipulate the cart and persist it in a database just after.

import { inject, injectable } from 'inversify'

@injectable()
export class CartService {
  @inject(TYPES.CartRepository) private repository: CartRepository

  private async _getCart(id: string): Promise<Cart> {
    try {
      const cart = await this.repository.getById(id)
      return cart
    } catch (e) {
      const emptyCart = Cart.create({ id, products: [] })
      return this.repository.create(emptyCart)
    }
  }

  public getById(id: string): Promise<Cart> {
    return this.repository.getById(id)
  }

  public async add(cartId: string, item: Item, sku: number): Promise<Cart> {
    const cart = await this._getCart(cartId)
    cart.add(item, sku)

    return this.repository.update(cart)
  }

  public async remove(cartId: string, itemId: string): Promise<Cart> {
    const cart = await this._getCart(cartId)
    cart.remove(itemId)

    return this.repository.update(cart)
  }
}

This layer is responsible only for operating the application. Changes made in the code here does not affect/break the entities or external dependencies such as databases.

Infrastructure

Even though at this point it might be very clear, but the infrastructure layer is responsible for every communication with external systems such as the system state (a database).

For saving data into the database, I use a mapper/repository approach.

A mapper can fetch raw database data and handle transformation to the matching domain class:

import { Cart, CartItem } from 'src/domain/cart'

const getProducts = (products: CartItem[]) => {
  return products.map(product => ({
    item: product.item,
    quantity: product.quantity
  }))
}

export class CartMapper {
  public static toDomain(raw: any): Cart {
    return Cart.create({
      id: raw.id,
      couponCode: raw.couponCode,
      products: getProducts(raw.products || [])
    })
  }
}

And the repository itself may depend on a database client library, which is a simple memory storage for this example, and use its methods for manipulating the data:

import { injectable, inject } from 'inversify'
import { Cart } from 'src/domain/cart'
import { CartMapper } from '../mappers/cart'

interface ICartRepository {
  getById(id: string): Promise<Cart>
  create(cart: Cart): Promise<Cart>
  update(cart: Cart): Promise<Cart>
}

@injectable()
export class CartRepository implements ICartRepository {
  @inject(TYPES.Database) private _database: MemoryData

  async getById(id: string): Promise<Cart> {
    const cart = await this._database.cart.getById(id)
    if (!cart) {
      throw new ResourceNotFound('Cart', { id })
    }
    return CartMapper.toDomain(cart)
  }

  async create(cart: Cart): Promise<Cart> {
    const dtoCart = cart.unmarshal()
    const inserted = await this._database.cart.insert(dtoCart)
    return CartMapper.toDomain(inserted)
  }

  async update(cart: Cart): Promise<Cart> {
    const dtoCart = cart.unmarshal()
    const updated = await this._database.cart.update(cart.id, dtoCart)
    return CartMapper.toDomain(updated)
  }
}

There is still space for improvements here. The code is just was made for quick demonstration porpuses, but one improvement for a good Onion would be having the interface for the repository defined inside the domain, and the implementation itself in the infrastructure layer we will discuss later.

Conclusion

The source code for this project is available at Github.