This document provides a comprehensive overview of the Domain-Driven Design (DDD) architecture implemented in this NestJS application.
- Architectural Layers
- Domain-Driven Design Principles
- CQRS Pattern
- Event-Driven Architecture
- Dependency Flow
- Module Structure
The application follows a layered architecture with clear separation of concerns:
Location: src/*/application/use-cases/*/controllers/
- Purpose: Handles HTTP requests and responses
- Responsibilities:
- Receive HTTP requests
- Validate input DTOs
- Dispatch commands/queries to the application layer
- Return HTTP responses
- Technologies: NestJS Controllers, Swagger, GraphQL
Example: CreateSingerController receives POST requests to create singers.
Location: src/*/application/
- Purpose: Orchestrates domain operations and coordinates use cases
- Components:
- Commands: Write operations (Create, Update, Delete)
- Queries: Read operations (GetById, GetByCriteria)
- Command Handlers: Execute commands and modify domain state
- Query Handlers: Retrieve and return data
- Sagas: Long-running processes that coordinate multiple operations
- DTOs: Data Transfer Objects for input/output
- Key Principle: Application layer should not contain business logic
Example: CreateSingerCommandHandler orchestrates the creation of a singer.
Location: src/*/domain/
- Purpose: Contains the core business logic and domain models
- Components:
- Aggregate Roots: Entities that manage consistency boundaries
- Entities: Domain objects with identity
- Value Objects: Immutable objects defined by their attributes
- Domain Events: Events that represent something that happened in the domain
- Business Rules: Validation and invariants enforced at the domain level
- Key Principle: Domain layer should be independent of infrastructure and application concerns
Example: Singer aggregate root enforces business rules like "cannot remove subscribed singer".
Location: src/*/infrastructure/
- Purpose: Provides technical capabilities to support other layers
- Components:
- Repositories: Implementations of domain repository interfaces
- Mappers: Convert between domain models and persistence models
- Database Tables: TypeORM entity definitions
- External Services: Integration with external systems
- Key Principle: Infrastructure layer depends on domain layer, not vice versa
Example: SingerRepository implements persistence operations for the Singer aggregate.
An Aggregate Root is an entity that controls access to a set of related objects (the aggregate). It ensures consistency and enforces invariants.
Example: Singer is an aggregate root that:
- Manages its own songs (entities)
- Enforces business rules (e.g., cannot remove subscribed singer)
- Publishes domain events when state changes
// Singer aggregate root
export class Singer extends DomainAggregateRoot<ISingerProps> {
// Business rules validation
protected businessRules(props: ISingerProps): void { ... }
// Domain operations
subscribe(audit: DomainAudit): this { ... }
remove(audit: DomainAudit): this { ... }
addSong(song: Song, audit: DomainAudit): this { ... }
}Entities are objects with unique identity that can change over time.
Example: Song is an entity within the Singer aggregate:
- Has a unique ID
- Can be created, modified, or removed
- Enforces its own business rules
Value Objects are immutable objects defined entirely by their attributes. Two value objects are equal if all their attributes are equal.
Examples:
FullName: Represents a singer's full namePicturePath: Represents a picture URLId: Represents a unique identifierName: Represents a song name
// Value object example
export class FullName extends AbstractDomainString {
// Immutable, validated on creation
static create(value: string): FullName { ... }
}Domain Events represent something that happened in the domain that domain experts care about.
Examples:
SingerCreatedDomainEvent: Published when a singer is createdSingerSubscribedDomainEvent: Published when a singer subscribesSingerDeletedDomainEvent: Published when a singer is deleted
// Domain event example
export class SingerCreatedDomainEvent extends DomainEvent {
constructor(
readonly singerId: string,
readonly singerName: string,
) { ... }
}Business Rules are invariants that must always be true. They are enforced at the domain level.
Example: In the Singer aggregate:
- If a singer is subscribed, a subscribed date is required
- A subscribed singer cannot be removed
- A singer cannot subscribe twice
CQRS (Command Query Responsibility Segregation) separates read and write operations:
- Purpose: Modify domain state
- Characteristics:
- Return void or minimal response
- May publish domain events
- Execute through CommandBus
- Location:
src/*/application/use-cases/commands/
Example Commands:
CreateSingerCommandChangeFullNameSingerCommandSubscribeSingerCommandRemoveSingerCommand
- Purpose: Retrieve data without modifying state
- Characteristics:
- Return data
- Should not have side effects
- Execute through QueryBus
- Location:
src/*/application/use-cases/queries/
Example Queries:
GetSingerByIdQueryGetSingersCriteriaQuery
- Domain Operation: A domain method is called (e.g.,
singer.subscribe()) - Event Creation: Domain event is added to the aggregate
- Event Publishing: When the aggregate is committed, events are published
- Event Handling: Event handlers react to events
- Saga Execution: Sagas may listen to events and trigger new commands
Location: src/*/application/use-cases/commands/*/created-*.domainevent-handler.ts
Event handlers react to domain events and can:
- Update read models
- Send notifications
- Trigger external integrations
- Update other aggregates
Location: src/*/application/sagas/
Sagas are long-running processes that:
- Listen to multiple domain events
- Coordinate complex workflows
- May trigger new commands
- Use RxJS for event stream processing
Example: SystemSagas listens to SingerCreatedDomainEvent and logs the event.
The dependency flow follows the Dependency Inversion Principle:
Presentation → Application → Domain ← Infrastructure
- Domain Layer: No dependencies on other layers
- Application Layer: Depends only on Domain
- Infrastructure Layer: Depends only on Domain
- Presentation Layer: Depends on Application
Location: src/shared/
Contains reusable components:
- Domain Primitives:
Id,Name,RegisterDate,SubscribedDate,Url - Base Classes:
AbstractCommandHandler - Exceptions:
DomainException,ApplicationException,DatabaseException - Context: Request context management
Location: src/*/ (e.g., src/singers/)
Each domain module is self-contained:
- Domain: Domain models and business logic
- Application: Use cases and orchestration
- Infrastructure: Technical implementations
Modules are registered in app.module.ts:
@Module({
imports: [
SharedModule,
SingersModule,
// ... other modules
],
})
export class AppModule {}- Keep Domain Pure: Domain layer should not depend on frameworks or infrastructure
- Enforce Business Rules: Business rules belong in the domain layer
- Use Value Objects: Prefer value objects over primitives for type safety
- Publish Domain Events: Use events to communicate between aggregates
- Separate Commands and Queries: Use CQRS for complex domains
- Validate at Boundaries: Validate input at the application layer
- Map at Boundaries: Convert between layers using mappers
- Domain Layer - Detailed domain model documentation
- Application Layer - Commands, queries, and handlers
- Infrastructure Layer - Repositories and persistence