@inovatechbg/base-ddd
TypeScript icon, indicating that this package has built-in type declarations

3.6.5 • Public • Published

Base DDD for TypeScript

GitHub Repo stars NPM Downloads

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)

Why DDD?

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.

Installation

NPM

npm install @inovatechbg/base-ddd

PNPM

pnpm add @inovatechbg/base-ddd

Yarn

yarn add @inovatechbg/base-ddd

Usage

Value Objects

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.

Complex Value Objects

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 }> {}

Value Object Constructor

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');

Value Object Getters

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

value getter

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

cleanValue getter

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 } }
CleanValueObject<T> type

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 }

Value Object Methods

The ValueObject class has some methods that you can use to compare two Value Objects.

equals method

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
Overriding the equals method

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

toString method

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

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> {}

Entity Constructor

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 });

Entity Getters

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

Entity Methods

The Entity class has some methods.

equals method

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
Overriding the equals method

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

Entity Identity

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);

Self Created Identity

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 });

Default Identities

The @inovatechbg/base-ddd/entities package has some default identities that you can use.

UniqueEntityId

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
IncrementalEntityId

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.

Creating You Own Identity

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';

Using Your Own Identity

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);

Entity vs Aggregate

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).

Watched List

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

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> {}

Aggregate extended from Entity

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

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).

Creating an Domain Event

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();
  }
}

Creating an Event Handler

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);
  }
}

Using the DomainEvents class

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.

register method

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);

dispatchEventsForAggregate method

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);

Creating a Domain Event in aggregate action

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 (Coming soon)

Repositories are used to store and retrieve Aggregates, Entities or Value Objects from the persistence layer.

Use Cases (Coming soon)

Use Cases are used to execute a business rule or a business flow.

Conclusion

This project is a base for a Domain Driven Design project using TypeScript.

Contributing

If you want to contribute to this project, you can fork this repository and create a pull request.

GitHub contributors

License

This project is licensed under the MIT License - see the LICENSE file for details.

Readme

Keywords

Package Sidebar

Install

npm i @inovatechbg/base-ddd

Weekly Downloads

5

Version

3.6.5

License

MIT

Unpacked Size

240 kB

Total Files

140

Last publish

Collaborators

  • inovatechbg