Getting started with Domain-Driven Design in TypeScript: a practical introduction
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.