Skip to main content

Domain Driven Design

Comprehensive Domain-Driven Design toolkit providing essential building blocks for robust, maintainable domain models.

Installation

npm install @globalart/ddd

Overview

The @globalart/ddd package provides a complete set of building blocks for implementing Domain-Driven Design patterns in your NestJS applications. It includes base classes, value objects, specifications, and utilities that help you build clean, maintainable domain models.

Key Features

  • Aggregate Root - Base class for domain aggregates with event management
  • Value Objects - Type-safe value objects with equality comparison
  • CQRS Support - Command and query base classes
  • Specifications - Composite specification pattern for business rules
  • Repository Pattern - Abstract repository interfaces
  • Domain Events - Event-driven architecture support
  • Type Safety - Full TypeScript support throughout

Quick Start

Aggregate Root

import { AggregateRoot } from '@globalart/ddd';

class UserCreatedEvent {
constructor(
public readonly userId: string,
public readonly email: string,
public readonly timestamp: Date = new Date()
) {}
}

class User extends AggregateRoot<UserCreatedEvent> {
constructor(
public readonly id: string,
public readonly email: string,
public readonly name: string
) {
super();
this.addDomainEvent(new UserCreatedEvent(id, email));
}

changeEmail(newEmail: string): void {
// Business logic here
this.addDomainEvent(new UserEmailChangedEvent(this.id, newEmail));
}
}

Value Objects

import { ValueObject } from '@globalart/ddd';

class Email extends ValueObject<{ value: string }> {
constructor(value: string) {
if (!this.isValidEmail(value)) {
throw new Error('Invalid email format');
}
super({ value });
}

private isValidEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

get value(): string {
return this.props.value;
}
}

// Usage
const email = new Email('user@example.com');
const emailValue = email.value; // 'user@example.com'

Commands and Queries

import { Command, Query } from '@globalart/ddd';

class CreateUserCommand extends Command {
constructor(
public readonly email: string,
public readonly name: string,
correlationId?: string
) {
super({ correlationId });
}
}

class GetUserQuery extends Query {
constructor(public readonly userId: string) {
super();
}
}

Core Components

Aggregate Root

Base class for domain aggregates that manages domain events:

import { AggregateRoot } from '@globalart/ddd';

abstract class AggregateRoot<EventType> {
protected addDomainEvent(event: EventType): void;
getDomainEvents(): EventType[];
clearDomainEvents(): void;
}

Value Objects

Type-safe value objects with built-in equality:

import { ValueObject } from '@globalart/ddd';

abstract class ValueObject<T> {
protected readonly props: T;

constructor(props: T);
equals(vo?: ValueObject<T>): boolean;
}

Built-in Value Objects

import { Id, NanoId, StringVO, BooleanVO, DateVO } from '@globalart/ddd';

// UUID-based identifier
const userId = new Id();

// NanoID-based identifier
const sessionId = new NanoId();

// String value object
const userName = new StringVO('john_doe');

// Boolean value object
const isActive = new BooleanVO(true);

// Date value object
const createdAt = new DateVO(new Date());

Specifications

Implement business rules using the specification pattern:

import { CompositeSpecification, Result, Ok, Err } from '@globalart/ddd';

class UserEmailSpecification extends CompositeSpecification<User> {
isSatisfiedBy(user: User): boolean {
return user.email.includes('@') && user.email.includes('.');
}

mutate(user: User): Result<User, string> {
return this.isSatisfiedBy(user)
? Ok(user)
: Err('Invalid email format');
}
}

// Usage
const emailSpec = new UserEmailSpecification();
const ageSpec = new UserAgeSpecification(18);

// Combine specifications
const validUserSpec = emailSpec.and(ageSpec);
const isValidUser = validUserSpec.isSatisfiedBy(user);

Filtering System

Advanced filtering with type-safe operations:

import { StringFilter, NumberFilter, DateFilter } from '@globalart/ddd';

// String filtering
const nameFilter = new StringFilter('name', 'contains', 'john');

// Number filtering
const ageFilter = new NumberFilter('age', 'gte', 18);

// Date filtering
const createdFilter = new DateFilter('createdAt', 'after', new Date('2023-01-01'));

// Combine filters
const combinedFilter = nameFilter.and(ageFilter).and(createdFilter);

Repository Pattern

Abstract repository interface for data access:

import { Repository } from '@globalart/ddd';

interface UserRepository extends Repository<User> {
findByEmail(email: Email): Promise<User | null>;
findActiveUsers(): Promise<User[]>;
save(user: User): Promise<void>;
delete(id: string): Promise<void>;
}

// Implementation
class TypeOrmUserRepository implements UserRepository {
async findByEmail(email: Email): Promise<User | null> {
// Implementation using TypeORM
}

async save(user: User): Promise<void> {
// Save user and publish domain events
}
}

Pagination Support

Built-in pagination with validation:

import { IPagination, ISort, paginationSchema } from '@globalart/ddd';

const pagination: IPagination = {
limit: 10,
offset: 0
};

const sorting: ISort = {
field: 'createdAt',
direction: 'DESC'
};

// Validation using Zod schema
const validatedPagination = paginationSchema.parse(pagination);

Advanced Usage

Domain Events

class OrderCreatedEvent {
constructor(
public readonly orderId: string,
public readonly customerId: string,
public readonly amount: number,
public readonly timestamp: Date = new Date()
) {}
}

class Order extends AggregateRoot<OrderCreatedEvent | OrderShippedEvent> {
constructor(
public readonly id: string,
public readonly customerId: string,
private _amount: number
) {
super();
this.addDomainEvent(new OrderCreatedEvent(id, customerId, _amount));
}

ship(): void {
// Business logic
this.addDomainEvent(new OrderShippedEvent(this.id));
}
}

Complex Specifications

class PremiumUserSpecification extends CompositeSpecification<User> {
isSatisfiedBy(user: User): boolean {
return user.subscriptionType === 'premium' && user.isActive;
}
}

class RecentActivitySpecification extends CompositeSpecification<User> {
constructor(private daysThreshold: number = 30) {
super();
}

isSatisfiedBy(user: User): boolean {
const daysSinceLastActivity = Date.now() - user.lastActivityAt.getTime();
return daysSinceLastActivity <= this.daysThreshold * 24 * 60 * 60 * 1000;
}
}

// Combine specifications
const eligibleForOfferSpec = new PremiumUserSpecification()
.and(new RecentActivitySpecification(7));

Best Practices

  • Keep Aggregates Small - Focus on consistency boundaries rather than data relationships
  • Use Value Objects - Encapsulate primitive values with business meaning
  • Implement Specifications - Use the specification pattern for complex business rules
  • Handle Domain Events - Use domain events for decoupling and side effects
  • Repository Abstraction - Keep domain logic independent of data access concerns
  • Validate at Boundaries - Use value objects and specifications to enforce invariants

Integration with NestJS

import { Injectable } from '@nestjs/common';
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';

@CommandHandler(CreateUserCommand)
@Injectable()
export class CreateUserHandler implements ICommandHandler<CreateUserCommand> {
constructor(private userRepository: UserRepository) {}

async execute(command: CreateUserCommand): Promise<void> {
const email = new Email(command.email);
const user = new User(new Id().value, email.value, command.name);

await this.userRepository.save(user);

// Domain events are handled automatically
}
}