@nivinjoseph/n-domain
Version:
Domain Driven Design and Event Sourcing based framework for business layer implementation
329 lines (255 loc) • 12 kB
Markdown
# 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
```bash
# Using npm
npm install @nivinjoseph/n-domain
# Using yarn
yarn add @nivinjoseph/n-domain
```
## Domain 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.ts
```
### Key Components
1. **Aggregate Root** (`aggregate.ts`)
- Main business entity
- Handles business logic
- Manages state changes through events
- Example: `Todo` aggregate
2. **State Management** (`aggregate-state.ts`)
- Defines the state interface
- Implements state factory
- Handles state transitions
- Example: `TodoState` and `TodoStateFactory`
3. **Domain Events** (`events/`)
- Represent state changes
- Immutable and serializable
- Follow naming convention: `AggregateActionEvent`
- Examples: `TodoCreated`, `TodoUpdated`, `TodoDeleted`
4. **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:
```typescript
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:
```typescript
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:
```typescript
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 context
- `id`: string - Unique identifier for the aggregate
- `retroEvents`: ReadonlyArray<DomainEvent<T>> - Historical events
- `retroVersion`: number - Version of historical events
- `currentEvents`: ReadonlyArray<DomainEvent<T>> - Current uncommitted events
- `currentVersion`: number - Current version of the aggregate
- `events`: ReadonlyArray<DomainEvent<T>> - All events (historical + current)
- `version`: number - Current version of the aggregate
- `createdAt`: number - Creation timestamp
- `updatedAt`: number - Last update timestamp
- `isNew`: boolean - Whether the aggregate is newly created
- `hasChanges`: boolean - Whether there are uncommitted changes
- `isReconstructed`: boolean - Whether the aggregate was reconstructed
- `reconstructedFromVersion`: number - Version from which the aggregate was reconstructed
- `isRebased`: boolean - Whether the aggregate was rebased
- `rebasedFromVersion`: 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 events
- `deserializeFromSnapshot(domainContext: DomainContext, aggregateType: new (...args: Array<any>) => TAggregate, stateFactory: AggregateStateFactory<TAggregateState>, stateSnapshot: TAggregateState | object)`: Static method to reconstruct an aggregate from a snapshot
- `snapshot(...cloneKeys: ReadonlyArray<string>)`: Create a snapshot of the current state
- `constructVersion(version: number)`: Construct the aggregate at a specific version
- `constructBefore(dateTime: number)`: Construct the aggregate before a specific timestamp
- `hasEventOfType(eventType: new (...args: Array<any>) => TEventType)`: Check if any event of a specific type exists
- `hasRetroEventOfType(eventType: new (...args: Array<any>) => TEventType)`: Check if any historical event of a specific type exists
- `hasCurrentEventOfType(eventType: new (...args: Array<any>) => TEventType)`: Check if any current event of a specific type exists
- `getEventsOfType(eventType: new (...args: Array<any>) => TEventType)`: Get all events of a specific type
- `getRetroEventsOfType(eventType: new (...args: Array<any>) => TEventType)`: Get all historical events of a specific type
- `getCurrentEventsOfType(eventType: new (...args: Array<any>) => TEventType)`: Get all current events of a specific type
- `clone(domainContext: DomainContext, createdEvent: DomainEvent<T>, serializedEventMutatorAndFilter?: (event: { $name: string; }) => boolean)`: Create a clone of the aggregate
- `rebase(version: number, rebasedEventFactoryFunc: (defaultState: object, rebaseState: object, rebaseVersion: number) => TDomainEvent)`: Rebase the aggregate to a specific version
- `applyEvent(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 to
- `id`: string - Unique identifier for the event
- `userId`: string - ID of the user who triggered the event
- `name`: string - Name of the event type
- `partitionKey`: 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 occurred
- `version`: number - Version number of the event
- `isCreatedEvent`: boolean - Whether this is a creation event
Key Methods:
- `apply(aggregate: AggregateRoot<T, DomainEvent<T>>, domainContext: DomainContext, state: T)`: Apply the event to an aggregate
- `applyEvent(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 aggregate
- `version`: number - Current version of the aggregate
- `createdAt`: number - Creation timestamp
- `updatedAt`: number - Last update timestamp
- `isDeleted`: boolean - Whether the aggregate is deleted
- `isRebased`: boolean - Whether the aggregate was rebased
- `rebasedFromVersion`: 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
1. **Event Design**
- Keep events immutable
- Include only necessary data
- Use meaningful event names
- Use `@serialize` decorator for serialization
- Implement proper validation using `given`
2. **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
3. **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
4. **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](LICENSE) file for details.
## Support
For issues and feature requests, please use the GitHub issue tracker.