Core domain primitives for event-driven architecture in TypeScript.
TypeScript-EDA Domain provides the fundamental building blocks for implementing Domain-Driven Design (DDD) patterns in event-driven architectures. This package contains the essential abstractions and tools for modeling rich business domains with clear separation of concerns.
- Rich Domain Entities with identity and behavior
- Immutable Value Objects with business rule validation
- Domain Events for capturing business occurrences
- Repository Abstractions for domain-friendly data access
- Port Definitions for clean architecture boundaries
- @listen Decorator for declarative event handling
- Type-Safe with full TypeScript support
npm install @typescript-eda/domain
# or
pnpm add @typescript-eda/domain
import { ValueObject } from '@typescript-eda/domain';
export class Email extends ValueObject {
constructor(private readonly value: string) {
super();
this.validate(value);
}
private validate(email: string): void {
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
throw new Error(`Invalid email: ${email}`);
}
}
public getValue(): string {
return this.value;
}
protected getEqualityComponents(): unknown[] {
return [this.value];
}
}
import { Event } from '@typescript-eda/domain';
export class UserRegistered extends Event {
public readonly type = 'UserRegistered';
constructor(
public readonly userId: string,
public readonly email: Email
) {
super();
}
public toJSON(): Record<string, unknown> {
return {
type: this.type,
userId: this.userId,
email: this.email.getValue(),
timestamp: this.timestamp.toISOString(),
id: this.id
};
}
}
import { Entity, listen } from '@typescript-eda/domain';
export class User extends Entity<UserId> {
constructor(
id: UserId,
private email: Email,
private status: UserStatus
) {
super(id);
}
@listen(EmailVerified)
public async verifyEmail(event: EmailVerified): Promise<void> {
if (!this.id.equals(event.userId)) return;
this.status = UserStatus.ACTIVE;
}
public changeEmail(newEmail: Email): EmailChangeRequested {
if (!this.status.isActive()) {
throw new Error('User must be active to change email');
}
this.email = newEmail;
return new EmailChangeRequested(this.id, newEmail);
}
}
import { Repository } from '@typescript-eda/domain';
export abstract class UserRepository extends Repository<User, UserId> {
public abstract findByEmail(email: Email): Promise<User | null>;
public abstract findActiveUsers(): Promise<User[]>;
}
Entities have identity that persists over time. They encapsulate business behavior and maintain consistency through their lifecycle.
export abstract class Entity<T extends ValueObject> {
constructor(protected readonly _id: T) {}
public get id(): T {
return this._id;
}
public equals(other: Entity<T>): boolean {
return this._id.equals(other._id);
}
}
Value Objects are immutable and defined by their attributes. They encapsulate business rules and prevent invalid states.
export abstract class ValueObject {
protected abstract getEqualityComponents(): unknown[];
public equals(other: ValueObject): boolean {
// Structural equality based on components
}
}
Events capture important business occurrences and enable loose coupling between domain components.
export abstract class Event {
public abstract readonly type: string;
public readonly timestamp: Date;
public readonly id: string;
public abstract toJSON(): Record<string, unknown>;
}
The @listen
decorator enables declarative event handling within entities:
export class Order extends Entity<OrderId> {
@listen(PaymentProcessed)
public async markAsPaid(event: PaymentProcessed): Promise<OrderPaid> {
// Handle payment processed event
}
}
TypeScript-EDA Domain follows clean architecture principles:
┌─────────────────────────────────────┐
│ Application Layer │ ← Uses domain abstractions
├─────────────────────────────────────┤
│ Domain Layer │ ← This package
│ • Entities • Value Objects │
│ • Events • Repositories │
│ • Services • Domain Rules │
├─────────────────────────────────────┤
│ Infrastructure Layer │ ← Implements domain contracts
└─────────────────────────────────────┘
- Keep entities focused on business behavior
- Use factories for complex creation logic
- Return events from state-changing operations
- Validate business rules within entities
- Make them immutable
- Validate in constructor
- Use factory methods for complex validation
- Include business logic relevant to the value
- Use past tense names (UserRegistered, not RegisterUser)
- Include all necessary data for event handlers
- Make events immutable
- Provide meaningful toJSON() implementations
- Define interfaces in domain layer
- Use domain language in method names
- Return domain objects, not DTOs
- Keep queries focused on business needs
Domain models are easy to test since they contain pure business logic:
describe('User', () => {
it('should verify email when verification event occurs', async () => {
const user = new User(userId, email, UserStatus.PENDING);
const event = new EmailVerified(userId);
await user.verifyEmail(event);
expect(user.getStatus()).toBe(UserStatus.ACTIVE);
});
});
See the examples directory for complete domain implementations:
- User Management: Registration, verification, and profile management
- E-commerce: Orders, products, and inventory
- Content Management: Articles, authors, and publishing workflows
- Getting Started Guide - Step-by-step tutorial
- Domain Story - The philosophy and evolution of domain design
- Development Journal - Design decisions and lessons learned
- Specifications - Complete domain examples
TypeScript-EDA Domain is part of the TypeScript-EDA ecosystem:
- @typescript-eda/infrastructure - Infrastructure adapters and implementations
- @typescript-eda/application - Application layer and dependency injection
- @web-buddy/core - Web automation built on EDA principles
We welcome contributions! Please see our Contributing Guide for details.
This project is licensed under the GPL-3.0 License - see the LICENSE file for details.
- Documentation: typescript-eda.org/domain
- Issues: GitHub Issues
- Discussions: GitHub Discussions