Getting started with Domain-Driven Design in TypeScript: a practical introduction

Alessandro Traversi
3 min readJul 17, 2023

--

In today’s software development world, Domain-Driven Design (DDD) stands as a strategic approach towards software design and architecture. A key idea of DDD is placing the project’s primary focus on the core domain and domain logic. The design then evolves around a conceptual model that tackles complex designs with simplicity.

Here, we’re going to explore how to structure a domain folder according to the principles of DDD, using TypeScript.

Overview of Domain-Driven Design

DDD consists of a series of layers:

Domain Layer:

The domain layer hosts business-specific entities, value objects, domain events, and aggregates.

Application Layer:

This layer drives the workflows of the entities and dictates the use cases of the application.

Infrastructure Layer:

The infrastructure layer interacts with the system and handles technical concerns like persistence, transactions, and so on.

In this article, we focus on the domain layer.

Domain Layer Structure in DDD

When we talk about the domain layer, we discuss Entities, Value Objects, Domain Events, and Aggregates. Let’s see how each of these might look in TypeScript.

Entities

Entities in DDD are unique objects in the domain model, which we identify by their ID rather than their attribute values. Below is a TypeScript example of a user entity:

export class User {
private _id: number;
private _name: string;

constructor(id: number, name: string) {
this._id = id;
this._name = name;
}

get id(): number {
return this._id;
}

get name(): string {
return this._name;
}
}

This entity class User has its attributes _id and _name and exposes them through getter methods.

Value Objects

Value objects are immutable and we identify them based on their attribute values rather than an ID. An example of a value object could be a user’s address:

export class Address {
private _street: string;
private _city: string;
private _zipCode: string;

constructor(street: string, city: string, zipCode: string) {
this._street = street;
this._city = city;
this._zipCode = zipCode;
}

get street(): string {
return this._street;
}

get city(): string {
return this._city;
}

get zipCode(): string {
return this._zipCode;
}
}

Domain Events

Domain events capture the outcome of actions taken in the domain model. An example could be a UserCreated event:

export class UserCreated {
private _userId: number;
private _occurredOn: Date;

constructor(userId: number) {
this._userId = userId;
this._occurredOn = new Date();
}

get userId(): number {
return this._userId;
}

get occurredOn(): Date {
return this._occurredOn;
}
}

Aggregates

Aggregates are clusters of associated objects that we treat as a unit for data changes. Each aggregate has a root, and external objects hold references to these roots.

Consider an Order aggregate:

import { User } from './User';
import { Address } from './Address';

export class Order {
private _user: User;
private _shippingAddress: Address;
private _items: Item[] = [];

constructor(user: User, shippingAddress: Address) {
this._user = user;
this._shippingAddress = shippingAddress;
}

addItem(item: Item): void {
this._items.push(item);
}

get user(): User {
return this._user;
}

get shippingAddress(): Address {
return this._shippingAddress;
}

get items(): Item[] {
return this._items;
}
}

In this example, Order is the aggregate root that holds the reference to User, Address, and Item entities.

Conclusion

This exploration provides an introduction to structuring a domain folder using Domain-Driven Design in TypeScript. In practice, DDD can go deeper, with richer possibilities and complexities. Still, this guide should give a solid starting point to build upon.

--

--

Alessandro Traversi

Senior Fullstack Developer | NodeJS | VueJS | ReactJS at Hotmart