This project has the purpose of providing a base for a Domain Driven Design (DDD) project using TypeScript.
This project implements ddd concepts like:
- Entities
- Aggregates
- Value Objects
- Domain Events
- Services (Coming soon)
- Repositories (Coming soon)
- Use Cases (Coming soon)
DDD is a software development approach that focuses on the domain and the business rules that govern it. It is a way to structure your code in a way that it is easier to understand and maintain.
npm install @inovatechbg/base-ddd
pnpm add @inovatechbg/base-ddd
yarn add @inovatechbg/base-ddd
If you want to set some validations, bussiness rules or even some formatting to a value, you can use a Value Object.
The Value Object is a class that extends the ValueObject
class from the @inovatechbg/base-ddd/value-objects
package.
import { ValueObject } from '@inovatechbg/base-ddd/value-objects';
class Name extends ValueObject<string> { }
The ValueObject
class has a value
property that is the value of the Value Object. This value property is is of the type that you define in the generic type of the ValueObject
class.
If you have a Value Object that has more than one property, you can create a class that extends the ValueObject
class and has the properties that you need.
import { ValueObject } from '@inovatechbg/base-ddd/value-objects';
class CustomVO extends ValueObject<{ name: string; age: number }> {}
class CustomComplexVO extends ValueObject<{ name: string; child: CustomVO }> {}
The Value Object constructor is a private constructor. You can't create a Value Object using the new
keyword.
To create a Value Object, you need to create a static method that will create the Value Object.
import { ValueObject } from '@inovatechbg/base-ddd/value-objects';
class Name extends ValueObject<string> {
static create(value: string): Name {
if (!value) {
throw new Error('Name is required');
}
if (value.length < 3) {
throw new Error('Name must have at least 3 characters');
}
return new Name(value);
}
}
const name = Name.create('John');
You can create getters in the Value Object to get some information from the value.
import { ValueObject } from '@inovatechbg/base-ddd/value-objects';
class Name extends ValueObject<string> {
static create(value: string): Name {
if (!value) {
throw new Error('Name is required');
}
if (value.length < 3) {
throw new Error('Name must have at least 3 characters');
}
return new Name(value);
}
get firstLetter(): string {
return this.value[0];
}
}
const name = Name.create('John');
console.log(name.firstLetter); // J
The value of a Value Object is a private and readonly property. You can't change the value of a Value Object after it is created.
To get the value of a Value Object, you can use the value
getter.
import { ValueObject } from '@inovatechbg/base-ddd/value-objects';
class Name extends ValueObject<string> {
static create(value: string): Name {
if (!value) {
throw new Error('Name is required');
}
if (value.length < 3) {
throw new Error('Name must have at least 3 characters');
}
return new Name(value);
}
}
const name = Name.create('John');
console.log(name.value); // John
The cleanValue
getter returns the value of the Value Object the more clean as possible (if the value is a primitive type, it will return like the value
getter. For more complex types, it will return as a clen object).
import { ValueObject } from '@inovatechbg/base-ddd/value-objects';
class CustomVO extends ValueObject<{ name: string; age: number }> {}
class CustomComplexVO extends ValueObject<{ name: string; child: CustomVO }> {}
const customVO = CustomVO.create({ name: 'John', age: 30 });
const customComplexVO = CustomComplexVO.create({ name: 'John', child: customVO });
console.log(customVO.cleanValue); // { name: 'John', age: 30 }
console.log(customComplexVO.cleanValue); // { name: 'John', child: { name: 'John', age: 30 } }
The CleanValueObject<T>
type is a type that will return the clean value of a Value Object.
import { CleanValueObject } from '@inovatechbg/base-ddd/value-objects';
class CustomVO extends ValueObject<{ name: string; age: number }> {}
const customVO = CustomVO.create({ name: 'John', age: 30 });
const cleanValue: CleanValueObject<CustomVO> = customVO.cleanValue;
console.log(cleanValue); // { name: 'John', age: 30 }
The ValueObject
class has some methods that you can use to compare two Value Objects.
The equals
method compares two Value Objects and returns a boolean.
import { ValueObject } from '@inovatechbg/base-ddd/value-objects';
class Name extends ValueObject<string> {
static create(value: string): Name {
if (!value) {
throw new Error('Name is required');
}
if (value.length < 3) {
throw new Error('Name must have at least 3 characters');
}
return new Name(value);
}
}
const name1 = Name.create('John');
const name2 = Name.create('John');
console.log(name1.equals(name2)); // true
You can override the equals
method to compare the Value Objects in a different way.
import { ValueObject } from '@inovatechbg/base-ddd/value-objects';
class Name extends ValueObject<string> {
static create(value: string): Name {
if (!value) {
throw new Error('Name is required');
}
if (value.length < 3) {
throw new Error('Name must have at least 3 characters');
}
return new Name(value);
}
equals(name: Name): boolean {
return this.value.toLowerCase() === name.value.toLowerCase();
}
}
const name1 = Name.create('John');
const name2 = Name.create('john');
console.log(name1.equals(name2)); // true
The toString
method returns the value of the Value Object.
import { ValueObject } from '@inovatechbg/base-ddd/value-objects';
class Name extends ValueObject<string> {
static create(value: string): Name {
if (!value) {
throw new Error('Name is required');
}
if (value.length < 3) {
throw new Error('Name must have at least 3 characters');
}
return new Name(value);
}
}
const name = Name.create('John');
console.log(name.toString()); // John
If the value of the Value Object is an object, the toString
method will return a JSON string.
Entities are objects that represent a unique object in the domain. The Entity has an identity that is unique in the domain.
The entity has props that are represented by native types, Value Objects or other Entities/Aggregates.
To create an Entity, you need to create a class that extends the Entity
class from the @inovatechbg/base-ddd/entities
package.
import { Entity } from '@inovatechbg/base-ddd/entities';
interface UserProps {
name: string;
age: number;
}
class User extends Entity<UserProps> {}
The Entity constructor is a private constructor. You can't create an Entity using the new
keyword.
To create an Entity, you need to create a static method that will create the Entity.
import { Entity } from '@inovatechbg/base-ddd/entities';
interface UserProps {
name: string;
age: number;
}
class User extends Entity<UserProps> {
static create(props: UserProps): User {
if (!props.name) {
throw new Error('Name is required');
}
if (!props.age) {
throw new Error('Age is required');
}
return new User(props);
}
}
const user = User.create({ name: 'John', age: 30 });
The Entity values are a protected atributte. You can't access the values directly.
To access the values of an Entity, you need to create getters.
import { Entity } from '@inovatechbg/base-ddd/entities';
interface UserProps {
name: string;
age: number;
}
class User extends Entity<UserProps> {
static create(props: UserProps): User {
if (!props.name) {
throw new Error('Name is required');
}
if (!props.age) {
throw new Error('Age is required');
}
return new User(props);
}
get name(): string {
return this.props.name;
}
get age(): number {
return this.props.age;
}
}
const user = User.create({ name: 'John', age: 30 });
console.log(user.name); // John
console.log(user.age); // 30
The Entity
class has some methods.
The equals
method compares two Entities and returns a boolean.
import { Entity } from '@inovatechbg/base-ddd/entities';
interface UserProps {
name: string;
age: number;
}
class User extends Entity<UserProps> {
static create(props: UserProps): User {
if (!props.name) {
throw new Error('Name is required');
}
if (!props.age) {
throw new Error('Age is required');
}
return new User(props);
}
}
const user1 = User.create({ name: 'John', age: 30 });
const user2 = User.create({ name: 'John', age: 30 });
console.log(user1.equals(user2)); // true
You can override the equals
method to compare the Entities in a different way.
import { Entity } from '@inovatechbg/base-ddd/entities';
interface UserProps {
name: string;
age: number;
}
class User extends Entity<UserProps> {
static create(props: UserProps): User {
if (!props.name) {
throw new Error('Name is required');
}
if (!props.age) {
throw new Error('Age is required');
}
return new User(props);
}
equals(user: User): boolean {
return this.props.name.toLowerCase() === user.props.name.toLowerCase();
}
}
const user1 = User.create({ name: 'John', age: 30 });
const user2 = User.create({ name: 'john', age: 30 });
console.log(user1.equals(user2)); // true
All of entities has an identity that is unique in this type of Entity in domain.
When you are creating an Entity, you need to pass the identity of the Entity as the second generic value. (default is UniqueEntityId, a UUID Value Object)
import { Entity } from '@inovatechbg/base-ddd/entities';
import { UniqueEntityId } from '@inovatechbg/base-ddd/value-objects';
interface UserProps {
name: string;
age: number;
}
class User extends Entity<UserProps, UniqueEntityId> {
static create(props: UserProps, id: UniqueEntityId): User {
if (!props.name) {
throw new Error('Name is required');
}
if (!props.age) {
throw new Error('Age is required');
}
return new User(props, id);
}
}
const id = UniqueEntityId.create();
const user = User.create({ name: 'John', age: 30 }, id);
If you want to create an Entity with a self created identity, you can pass the identity contructor as the second contructor value of Entity constructor.
import { Entity } from '@inovatechbg/base-ddd/entities';
import { UniqueEntityId, UniqueEntityIdConstructor } from '@inovatechbg/base-ddd/value-objects';
interface UserProps {
name: string;
age: number;
}
class User extends Entity<UserProps, UniqueEntityId> {
static create(props: UserProps, id: UniqueEntityId): User {
if (!props.name) {
throw new Error('Name is required');
}
if (!props.age) {
throw new Error('Age is required');
}
return new User(props, id ?? new UniqueEntityIdConstructor());
}
}
const user = User.create({ name: 'John', age: 30 });
The @inovatechbg/base-ddd/entities
package has some default identities that you can use.
The UniqueEntityId
is a Value Object that has a UUID as the value.
import { UniqueEntityId } from '@inovatechbg/base-ddd/value-objects';
const id = UniqueEntityId.create();
console.log(id.value); // 123e4567-e89b-12d3-a456-426614174000
The IncrementalEntityId
is a Value Object that has a number as the value.
import { IncrementalEntityId } from '@inovatechbg/base-ddd/value-objects';
const id = IncrementalEntityId.create();
console.log(id.value); // -1
The IncrementalEntityId
return -1 as the default value.
If you want to create your own identity, you can create a class that extends the Id
class from the @inovatechbg/base-ddd/entities
package, passing the type of the identity as the generic type.
You need to create a method id
that will return the identity value.
import { Id } from '@inovatechbg/base-ddd/value-objects';
class UserId extends Id<string> {
id(): string {
return this.value;
}
}
Is recommended to create another class with same name adding Constructor
to create the identity.
This class muts extends the IdConstructor
class from the @inovatechbg/base-ddd/entities
package.
If you did that, you must to implement a method create
that will receive the identity value and return a new instance of the identity.
import { IdConstructor } from '@inovatechbg/base-ddd/entities';
class UserIdConstructor extends IdConstructor<UserId> {
create(value: string): UserId {
return new UserId(value);
}
}
You also have a type IdType<Type>
that will return the value type of the identity.
import { IdType } from '@inovatechbg/base-ddd/entities';
import { Id } from '@inovatechbg/base-ddd/value-objects';
class UserId extends Id<string> {}
const id: IdType<UserId> = '123e4567-e89b-12d3-a456-426614174000';
To use your own identity, you need to pass the identity class as the second generic value of the Entity constructor.
import { Entity } from '@inovatechbg/base-ddd/entities';
import { UserId, UserIdConstructor } from './UserId';
interface UserProps {
name: string;
age: number;
}
class User extends Entity<UserProps, UserId> {
static create(props: UserProps, id: UserId): User {
if (!props.name) {
throw new Error('Name is required');
}
if (!props.age) {
throw new Error('Age is required');
}
return new User(props, id);
}
}
const id = new UserIdConstructor().create('123e4567-e89b-12d3-a456-426614174000');
const user = User.create({ name: 'John', age: 30 }, id);
If you would like to use a Entity list as a property of another Entity, the main entity should be a Aggregate.
But, the child entity list can be a normal array.
The same concept can be applied to single entities.
That will be more explained in the Aggregates section, but pay attention if the entity need to be a Aggregate (having a child entity), or only an entity with a reference to another entity as an Id (like a foreign key in a database).
If you want to turn the lists more useful, you can use Watched Lists.
We are working on another package to provide this feature. (Coming soon)
Aggregates are a group of entities that are treated as a single unit.
The Aggregate has an Entity that is the root of the Aggregate. The root Entity is the only Entity that should be accessed from outside the Aggregate.
To create an Aggregate, you need to create a class that extends the AggregateRoot
class from the @inovatechbg/base-ddd/entities
package.
import { AggregateRoot } from '@inovatechbg/base-ddd/entities';
import { Attachment } from './Attachment';
interface EmailProps {
subject: string;
body: string;
attachments: Attachment[];
}
class Email extends AggregateRoot<EmailProps> {}
The Aggregate is an Entity with some more features and concepts applied. So, things like Construcotr, Getters, Methods and Identity can be used in the Aggregate as the same way.
import { AggregateRoot } from '@inovatechbg/base-ddd/entities';
interface EmailProps {
subject: string;
body: string;
}
class Email extends AggregateRoot<EmailProps> {
static create(props: EmailProps): Email {
if (!props.subject) {
throw new Error('Subject is required');
}
if (!props.body) {
throw new Error('Body is required');
}
return new Email(props);
}
get subject(): string {
return this.props.subject;
}
get body(): string {
return this.props.body;
}
}
const email = Email.create({ subject: 'Hello', body: 'Hello, World!' });
console.log(email.subject); // Hello
console.log(email.body); // Hello, World!
Domain Events are events that are triggered when something happens in the domain.
All of the Domain Events should be created based on Aggregate action (like creation, uptade, or deletion). But should be executed just after the action is completed (like a transaction).
To create a Domain Event, you need to create a class that implements the DomainEvent
interface from the @inovatechbg/base-ddd/events
package.
The domain event must have the attribute ocurredAt
that is a Date object with the time that the event ocurred.
The domain event also must have an method getAggregateId
that will return the aggregate id that the event is related.
Is recommended to add an private attribute aggregate with the full aggregate, to be used to get the aggregate id.
import { DomainEvent } from '@inovatechbg/base-ddd/events';
class UserCreated implements DomainEvent {
ocurredAt: Date;
private aggregate: User;
constructor(aggregate: User) {
this.ocurredAt = new Date();
this.aggregate = aggregate;
}
getAggregateId(): string {
return this.aggregate.id.id();
}
}
To handle the Domain Event, you need to create a class that implements the EventHandler
interface from the @inovatechbg/base-ddd/events
package.
You need to pass the Domain Event class that the handler will handle as the generic type of the EventHandler.
The handler must have a method handle
that will receive the Domain Event and do something with it.
You can receive anythig in the Event Handler constructor, like a repository, a service or anything that you need to handle the event.
as the parameter of the handle method, you will receive the event, and do anything with it.
import { EventHandler } from '@inovatechbg/base-ddd/events';
import { UserCreated } from './UserCreated';
class UserCreatedHandler implements EventHandler<UserCreated> {
constructor(private emailService: EmailService, private userRepository: UserRepository) {}
async handle(event: UserCreated): Promise<void> {
const user = await this.userRepository.findById(event.getAggregateId());
await this.emailService.sendWelcomeEmail(user.email);
}
}
To use the Domain Events, you need to use the DomainEvents
class from the @inovatechbg/base-ddd/events
package.
The DomainEvents
class has some methods that you can use to interact with the Domain Events.
The register
method is used to register a Event Handler, passing the Event Handler class as the parameter.
You also need to pass the name of the event that the handler will handle.
import { DomainEvents } from '@inovatechbg/base-ddd/events';
import { UserCreatedHandler } from './UserCreatedHandler';
import { UserCreated } from './UserCreated';
import { EmailService } from './EmailService';
import { UserRepository } from './UserRepository';
const emailService = new EmailService();
const userRepository = new UserRepository();
const userCreatedHandler = new UserCreatedHandler(emailService, userRepository);
DomainEvents.register(userCreatedHandler, UserCreated.name);
The dispatchEventsForAggregate
method is used to dispatch the Domain Events for an Aggregate.
You need to pass the aggregate.id as the parameter.
import { DomainEvents } from '@inovatechbg/base-ddd/events';
DomainEvents.dispatchEventsForAggregate(aggregate.id);
To create a Domain Event in an Aggregate action, in the action inside or outside the aggregate, you need to call the method addDomainEvent
from the aggregate.
This method will add the Domain Event to the aggregate and to DomainEvents wait list.
So, if you call the dispatchEventsForAggregate
method, the Domain Event will be dispatched.
import { AggregateRoot } from '@inovatechbg/base-ddd/entities';
import { DomainEvents } from '@inovatechbg/base-ddd/events';
import { UserCreated } from './UserCreated';
interface UserProps {
name: string;
age: number;
}
class User extends AggregateRoot<UserProps> {
static create(props: UserProps): User {
if (!props.name) {
throw new Error('Name is required');
}
if (!props.age) {
throw new Error('Age is required');
}
const user = new User(props);
user.addDomainEvent(new UserCreated(user));
return user;
}
get name(): string {
return this.props.name;
}
get age(): number {
return this.props.age;
}
}
const user = User.create({ name: 'John', age: 30 });
DomainEvents.dispatchEventsForAggregate(user.id);
Repositories are used to store and retrieve Aggregates, Entities or Value Objects from the persistence layer.
Use Cases are used to execute a business rule or a business flow.
This project is a base for a Domain Driven Design project using TypeScript.
If you want to contribute to this project, you can fork this repository and create a pull request.
This project is licensed under the MIT License - see the LICENSE file for details.