Skip to main content

Domain-Driven Design

The @axisparkjs/ddd package provides small TypeScript building blocks for expressing domain concepts. Its base classes help define value objects, identifiers, entities, aggregate roots, domain events, and domain-specific exceptions. They do not persist objects or publish events; applications decide how to store aggregates and dispatch events.

Install and import​

npm install @axisparkjs/ddd

Import the building blocks from the package root:

import { AggregateRoot, DomainEvent, DomainException, Entity, Identifier, ValueObject } from '@axisparkjs/ddd';

For the generated API reference, see @axisparkjs/ddd.

Value objects​

A value object is compared by the value it wraps rather than by object identity. Extend ValueObject<T>, validate values in validate, and provide a toString representation:

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

export class EmailAddress extends ValueObject<string> {
protected validate(value: string): void {
if (!value.includes('@')) throw new Error('Email address must contain @');
}

public toString(): string {
return this.value;
}
}

const first = new EmailAddress('ada@example.com');
const second = new EmailAddress('ada@example.com');
first.equals(second); // true

The base constructor calls validate before storing the value. equals compares the wrapped values by their JSON serialization.

Identifiers and entities​

Use Identifier<T> to give an identifier its own domain type. It inherits value-object validation and equality, and implements toString() by converting its value to a string:

import { Identifier } from '@axisparkjs/ddd';

export class CustomerId extends Identifier<string> {
protected validate(value: string): void {
if (value.trim().length === 0) throw new Error('Customer ID cannot be empty');
}
}

Extend Entity<TIdentifier> for domain objects with identity. Its equals method returns true only when both entities have the same concrete class and equal identifiers.

import { Entity } from '@axisparkjs/ddd';
import { CustomerId } from './customer-id';

export class Customer extends Entity<CustomerId> {
constructor(
id: CustomerId,
public name: string
) {
super(id);
}
}

const customer = new Customer(new CustomerId('customer-42'), 'Ada');
customer.id.toString(); // 'customer-42'

Aggregate roots and domain events​

An aggregate root is an entity that protects an aggregate's behavior and records events raised while that behavior runs. Extend AggregateRoot<TIdentifier> and add events with addDomainEvent:

import { AggregateRoot, DomainEvent } from '@axisparkjs/ddd';
import { CustomerId } from './customer-id';

export class CustomerRenamed extends DomainEvent {
constructor(
public readonly customerId: CustomerId,
public readonly name: string
) {
super();
}
}

export class Customer extends AggregateRoot<CustomerId> {
constructor(
id: CustomerId,
public name: string
) {
super(id);
}

public rename(name: string): void {
this.name = name;
this.addDomainEvent(new CustomerRenamed(this.id, name));
}
}

const customer = new Customer(new CustomerId('customer-42'), 'Ada');
customer.rename('Ada Lovelace');

const events = customer.pullDomainEvents(); // returns pending events and clears the queue
events[0].occurredOn; // Date recorded when the event was created

pullDomainEvents() returns a copy of the pending event list and empties the aggregate's queue. Call clearDomainEvents() to discard pending events. HasDomainEvents describes the pull method for objects that expose domain events. The package does not dispatch the returned events; for example, an application can collect them after saving an aggregate and pass them to its own publisher.

Domain exceptions​

Extend DomainException for errors that represent invalid domain operations or state. The base class sets the error name to the subclass name:

import { DomainException } from '@axisparkjs/ddd';

export class CustomerNameRequired extends DomainException {
constructor() {
super('Customer name is required');
}
}

throw new CustomerNameRequired();

Public API​

ExportRole
ValueObject<T>Validated value with value, equals, and abstract toString().
Identifier<T = string>Value-object base for domain-specific identifiers.
Entity<T extends Identifier>Base class with an identifier and identity-based equality.
AggregateRoot<T extends Identifier>Entity that collects and exposes domain events.
DomainEventBase class with an occurredOn timestamp.
HasDomainEventsInterface with pullDomainEvents().
DomainExceptionBase class for domain errors.