@nivinjoseph/n-domain
v3.0.3
Published
Domain Driven Design and Event Sourcing based framework for business layer implementation
Downloads
3,778
Readme
n-domain
Overview
n-domain is a TypeScript framework that provides a robust foundation for implementing business logic using Domain-Driven Design (DDD) and Event Sourcing patterns. It helps you create maintainable and scalable domain models while enforcing best practices in domain-driven design.
Features
- Domain-Driven Design Support: Built-in abstractions for DDD concepts like Aggregates, Entities, and Domain Events
- Event Sourcing: Native support for event-sourced aggregates and state management
- Type Safety: Written in TypeScript with strong typing support
- Flexible Configuration: Configurable domain contexts and state management
- Clean Architecture: Promotes separation of concerns and clean architecture principles
Installation
# Using npm
npm install @nivinjoseph/n-domain
# Using yarn
yarn add @nivinjoseph/n-domainDomain Organization
The framework encourages a clean and organized domain structure. Here's how to organize your domain:
domain/
├── aggregate.ts # Main aggregate root implementation
├── aggregate-state.ts # State interface and factory
├── events/ # Domain events
│ ├── aggregate-created.ts
│ ├── aggregate-updated.ts
│ └── aggregate-deleted.ts
└── value-objects/ # Value objects
├── description.ts
└── other-value-objects.tsKey Components
Aggregate Root (
aggregate.ts)- Main business entity
- Handles business logic
- Manages state changes through events
- Example:
Todoaggregate
State Management (
aggregate-state.ts)- Defines the state interface
- Implements state factory
- Handles state transitions
- Example:
TodoStateandTodoStateFactory
Domain Events (
events/)- Represent state changes
- Immutable and serializable
- Follow naming convention:
AggregateActionEvent - Examples:
TodoCreated,TodoUpdated,TodoDeleted
Value Objects (
value-objects/)- Immutable objects
- No identity
- Represent domain concepts
- Examples:
TodoDescription,Address,Money
Core Concepts
Aggregate Roots
Aggregate roots are the main building blocks of your domain model. They encapsulate business logic and ensure consistency boundaries. Example:
import { given } from "@nivinjoseph/n-defensive";
import { AggregateRoot, DomainContext, DomainEvent } from "@nivinjoseph/n-domain";
import { TodoCreated } from "./events/todo-created";
import { TodoState, TodoStateFactory } from "./todo-state";
@serialize("Test")
export class Todo extends AggregateRoot<TodoState, TodoDomainEvent>
{
public get title(): string { return this.state.title; }
public get description(): string | null { return this.state.description?.description ?? null; }
public get isCompleted(): boolean { return this.state.isCompleted; }
public constructor(domainContext: DomainContext, events: ReadonlyArray<DomainEvent<TodoState>>, state?: TodoState)
{
super(domainContext, events, new TodoStateFactory(), state);
}
public static create(domainContext: DomainContext, title: string, description: string | null): Todo
{
given(domainContext, "domainContext").ensureHasValue().ensureIsObject();
given(title, "title").ensureHasValue().ensureIsString();
given(description as string, "description").ensureIsString();
return new Todo(domainContext, [new TodoCreated({
todoId: DomainHelper.generateId("tdo"),
title,
description: description != null ? TodoDescription.create(description) : null
})]);
}
public updateTitle(title: string): void
{
given(title, "title").ensureHasValue().ensureIsString();
title = title.trim();
this.applyEvent(new TodoTitleUpdated({ title }));
}
}Domain Events
Domain events represent state changes in your aggregates. They are immutable and carry the data necessary to modify the aggregate state:
import { given } from "@nivinjoseph/n-defensive";
import { serialize } from "@nivinjoseph/n-util";
import { DomainEventData } from "@nivinjoseph/n-domain";
import { TodoState } from "../todo-state";
@serialize("Test")
export class TodoCreated extends TodoDomainEvent
{
private readonly _todoId: string;
private readonly _title: string;
@serialize
public get todoId(): string { return this._todoId; }
@serialize
public get title(): string { return this._title; }
@serialize
public get description(): TodoDescription | null { return this._description; }
public constructor(data: EventData)
{
given(data, "data").ensureHasValue().ensureIsObject();
data.$isCreatedEvent = true;
super(data);
const { todoId, title } = data;
given(todoId, "todoId").ensureHasValue().ensureIsString();
this._todoId = todoId;
given(title, "title").ensureHasValue().ensureIsString();
this._title = title;
}
protected applyEvent(state: TodoState): void
{
given(state, "state").ensureHasValue().ensureIsObject();
state.id = this._todoId;
state.title = this._title;
}
}
interface EventData extends DomainEventData
{
todoId: string;
title: string;
}State Management
State management is handled through state interfaces and factories:
import { AggregateState } from "@nivinjoseph/n-domain";
import { AggregateStateFactory } from "@nivinjoseph/n-domain";
import { TodoDescription } from "./value-objects/todo-description";
export interface TodoState extends AggregateState
{
title: string;
description: TodoDescription | null;
isCompleted: boolean;
}
export class TodoStateFactory extends AggregateStateFactory<TodoState>
{
public create(): TodoState
{
return {
...this.createDefaultAggregateState(),
title: null as any,
description: null,
isCompleted: false
};
}
}API Reference
AggregateRoot
Base class for aggregate roots in your domain model.
Properties:
context: DomainContext - The domain contextid: string - Unique identifier for the aggregateretroEvents: ReadonlyArray<DomainEvent> - Historical eventsretroVersion: number - Version of historical eventscurrentEvents: ReadonlyArray<DomainEvent> - Current uncommitted eventscurrentVersion: number - Current version of the aggregateevents: ReadonlyArray<DomainEvent> - All events (historical + current)version: number - Current version of the aggregatecreatedAt: number - Creation timestampupdatedAt: number - Last update timestampisNew: boolean - Whether the aggregate is newly createdhasChanges: boolean - Whether there are uncommitted changesisReconstructed: boolean - Whether the aggregate was reconstructedreconstructedFromVersion: number - Version from which the aggregate was reconstructedisRebased: boolean - Whether the aggregate was rebasedrebasedFromVersion: number - Version from which the aggregate was rebased
Key Methods:
deserializeFromEvents(domainContext: DomainContext, aggregateType: new (...args: Array<any>) => TAggregate, eventData: ReadonlyArray<DomainEventData>): Static method to reconstruct an aggregate from eventsdeserializeFromSnapshot(domainContext: DomainContext, aggregateType: new (...args: Array<any>) => TAggregate, stateFactory: AggregateStateFactory<TAggregateState>, stateSnapshot: TAggregateState | object): Static method to reconstruct an aggregate from a snapshotsnapshot(...cloneKeys: ReadonlyArray<string>): Create a snapshot of the current stateconstructVersion(version: number): Construct the aggregate at a specific versionconstructBefore(dateTime: number): Construct the aggregate before a specific timestamphasEventOfType(eventType: new (...args: Array<any>) => TEventType): Check if any event of a specific type existshasRetroEventOfType(eventType: new (...args: Array<any>) => TEventType): Check if any historical event of a specific type existshasCurrentEventOfType(eventType: new (...args: Array<any>) => TEventType): Check if any current event of a specific type existsgetEventsOfType(eventType: new (...args: Array<any>) => TEventType): Get all events of a specific typegetRetroEventsOfType(eventType: new (...args: Array<any>) => TEventType): Get all historical events of a specific typegetCurrentEventsOfType(eventType: new (...args: Array<any>) => TEventType): Get all current events of a specific typeclone(domainContext: DomainContext, createdEvent: DomainEvent<T>, serializedEventMutatorAndFilter?: (event: { $name: string; }) => boolean): Create a clone of the aggregaterebase(version: number, rebasedEventFactoryFunc: (defaultState: object, rebaseState: object, rebaseVersion: number) => TDomainEvent): Rebase the aggregate to a specific versionapplyEvent(event: TDomainEvent): Apply a new event to the aggregate
DomainEvent
Base class for domain events.
Properties:
aggregateId: string - ID of the aggregate this event belongs toid: string - Unique identifier for the eventuserId: string - ID of the user who triggered the eventname: string - Name of the event typepartitionKey: string - Same as aggregateId (for n-eda compatibility)refId: string - Same as aggregateId (for n-eda compatibility)refType: string - Abstract property to be implemented (for n-eda compatibility)occurredAt: number - Timestamp when the event occurredversion: number - Version number of the eventisCreatedEvent: boolean - Whether this is a creation event
Key Methods:
apply(aggregate: AggregateRoot<T, DomainEvent<T>>, domainContext: DomainContext, state: T): Apply the event to an aggregateapplyEvent(state: T): Abstract method to be implemented for applying event-specific changes to state
AggregateState
Base interface for aggregate state.
Properties:
id: string - Unique identifier for the aggregateversion: number - Current version of the aggregatecreatedAt: number - Creation timestampupdatedAt: number - Last update timestampisDeleted: boolean - Whether the aggregate is deletedisRebased: boolean - Whether the aggregate was rebasedrebasedFromVersion: number - Version from which the aggregate was rebased
DomainContext
Interface for domain context.
Properties:
userId: string - ID of the current user
ConfigurableDomainContext
Class for configuring domain context.
Properties:
userId: string - ID of the current user
Methods:
configure(userId: string): Configure the domain context with a user ID
Best Practices
Event Design
- Keep events immutable
- Include only necessary data
- Use meaningful event names
- Use
@serializedecorator for serialization - Implement proper validation using
given
Aggregate Design
- Keep aggregates focused and cohesive
- Maintain consistency boundaries
- Use event sourcing for complex state management
- Implement proper validation in all public methods
- Use static factory methods for creation
State Management
- Use the built-in state management helpers
- Implement proper event handlers
- Consider performance implications of event history
- Use proper typing for state interfaces
Domain Organization
- Keep related files close together
- Use clear naming conventions
- Separate concerns into appropriate directories
- Maintain a flat structure for better discoverability
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Support
For issues and feature requests, please use the GitHub issue tracker.
