Domain Modeling and Aggregate Design in Intent
Overview
Domain-Driven Design (DDD) is a core architectural approach in Intent. The system models the business domain using aggregates, which are clusters of domain objects treated as a single unit for data changes. This approach ensures that business rules are enforced consistently and that the system accurately reflects the real-world domain it represents.
Aggregate Pattern Implementation
BaseAggregate Abstract Class
The foundation of domain modeling in the system is the BaseAggregate
abstract class defined in src/core/shared/aggregate.ts
:
export abstract class BaseAggregate<TState> { abstract aggregateType: string; version = 0; constructor(public id: UUID) {} // Schema versioning static CURRENT_SCHEMA_VERSION = 1; // Abstract methods that must be implemented by concrete aggregates abstract apply(event: any, isSnapshot?: boolean): void; abstract handle(command: any): Event[]; protected abstract applyUpcastedSnapshot(state: TState): void; abstract extractSnapshotState(): TState; // Methods for snapshot handling applySnapshotState(raw: any, incomingVersion?: number): void { /* ... */ } toSnapshot(): Snapshot<TState> { /* ... */ } static fromSnapshot<T extends BaseAggregate<any>>(this: new (id: UUID) => T, event: any): T { /* ... */ } }
Key features of the BaseAggregate
class:
- Generic State Type: Uses a generic type parameter
TState
to define the shape of the aggregate's state - Version Tracking: Maintains a version number for optimistic concurrency control
- Schema Versioning: Supports evolving the aggregate's schema over time
- Event Sourcing Integration: Provides methods for applying events and creating snapshots
- Command Handling: Requires concrete implementations to handle commands and produce events
Concrete Aggregate Example: SystemAggregate
The SystemAggregate
class in src/core/example-slices/system/aggregates/system.aggregate.ts
demonstrates how to implement a concrete aggregate:
export class SystemAggregate extends BaseAggregate<SystemSnapshotState> { public aggregateType = 'system'; static CURRENT_SCHEMA_VERSION = 1; // State properties id: UUID; version = 0; numberExecutedTests = 0; // Static factory methods static create(cmd: Command<...>): SystemAggregate { /* ... */ } static rehydrate(events: Event[]): SystemAggregate { /* ... */ } // Command handling private readonly handlers: Record<SystemCommandType, (cmd: Command) => Event[]> = { /* ... */ }; public handle(cmd: Command): Event[] { /* ... */ } // Event application public apply(event: Event, isNew = true): void { /* ... */ } // Specific command handlers private handleLogMessage(cmd: Command<LogMessagePayload>): Event[] { /* ... */ } private handleExecuteTest(cmd: Command<ExecuteTestPayload>): Event[] { /* ... */ } // ... other command handlers // Specific event handlers private applyMessageLogged(_: Event<MessageLoggedPayload>): void { /* ... */ } private applyTestExecuted(_: Event<TestExecutedPayload>): void { /* ... */ } // ... other event handlers // Snapshot methods protected upcastSnapshotState(raw: any, version: number): SystemSnapshotState { /* ... */ } protected applyUpcastedSnapshot(state: SystemSnapshotState): void { /* ... */ } extractSnapshotState(): SystemSnapshotState { /* ... */ } }
Key features of the SystemAggregate
implementation:
- State Definition: Defines a specific state type (
SystemSnapshotState
) - Command Handler Registry: Uses a map of command types to handler methods
- Event Application: Dispatches events to specific handlers based on event type
- Business Rules: Enforces business rules in command handlers
- Access Control: Integrates with the policy system for authorization checks
- Factory Methods: Provides static methods for creating and rehydrating aggregates
Command Processing Flow
The command processing flow involves several components:
- Command Bus: Routes commands to appropriate handlers
- Command Handlers: Domain services that implement the
CommandHandler
interface - Aggregates: Domain entities that encapsulate business rules and state changes
- Event Generation: Commands result in events that represent state changes
The SystemCommandHandler
class shows how command handlers interact with aggregates:
export class SystemCommandHandler implements CommandHandler<Command<any>> { supportsCommand(cmd: Command): boolean { return Object.values(SystemCommandType).includes(cmd.type as SystemCommandType); } async handleWithAggregate(cmd: Command<any>, aggregate: BaseAggregate<any>): Promise<Event<any>[]> { if (!(aggregate instanceof SystemAggregate)) { throw new Error(`Expected SystemAggregate but got ${aggregate.constructor.name} for cmd: ${cmd.type}`); } return aggregate.handle(cmd); } }
Aggregate Registry
The system maintains a registry of aggregate types in src/core/registry.ts
:
This registry allows the system to dynamically create and load aggregates based on their type.
Benefits of the Aggregate Pattern
- Encapsulation: Business rules are encapsulated within the aggregate
- Consistency: Aggregates ensure consistency boundaries are maintained
- Event Sourcing Integration: The design works seamlessly with event sourcing
- Versioning Support: Built-in support for schema evolution
- Clear Responsibility: Each aggregate has a clear responsibility in the domain
Domain Modeling Principles
Intent follows several key domain modeling principles:
- Ubiquitous Language: The code encourages using domain terminology consistently
- Bounded Contexts: The system is organized into distinct domain slices
- Aggregates as Consistency Boundaries: Each aggregate enforces its own consistency rules
- Rich Domain Model: Business logic is in the domain model, not in application services
- Separation of Concerns: Clear separation between domain logic and infrastructure
Integration with Other Patterns
Domain modeling in Intent integrates with several other patterns:
- Event Sourcing: Aggregates are the source of events
- CQRS: Aggregates are part of the write model
- Temporal Workflows: Complex processes may involve multiple aggregates
- Multi-tenancy: Maintain tenant isolation through and through; from commands to events, aggregates and projections