Feature Implementation Guide #
This guide outlines the vertical slice approach for implementing features end-to-end in our DDD (Domain-Driven Design) and layered architecture system with Command Query Responsibility Segregation (CQRS).
Architecture Overview #
Our system follows a clean architecture pattern with the following layers:
- Domain Layer: Core business logic, entities, value objects, and domain services
- Application Layer: Use cases (commands and queries), DTOs, and application services
- Infrastructure Layer: Repositories, external services, and technical implementations
- Presentation Layer: HTTP controllers, routes, and API clients
Command vs Query Pattern #
We implement CQRS to separate read and write operations:
Commands #
- Purpose: Modify system state (Create, Update, Delete operations)
- Example:
AddUrlToLibraryUseCase - Characteristics:
- Return success/failure results
- May trigger domain events
- Often involve business rule validation
- Use domain services for complex operations
Queries #
- Purpose: Read data without side effects
- Example:
GetCollectionPageUseCase - Characteristics:
- Return data transfer objects (DTOs)
- Optimized for specific read scenarios
- May aggregate data from multiple sources
- Use query repositories for efficient data access
Vertical Slice Implementation Steps #
1. Domain Layer (if needed) #
Domain Entities & Value Objects #
- Location:
src/modules/{module}/domain/ - Files: Entities, Value Objects, Domain Services
- Example:
Collection.ts,CardId.ts,URL.ts
Domain Services #
- Location:
src/modules/{module}/domain/services/ - Purpose: Complex business logic that doesn't belong to a single entity
- Example:
CardLibraryService,CardCollectionService
Repository Interfaces #
- Location:
src/modules/{module}/domain/ - Files:
I{Entity}Repository.ts- For command operations (write/modify state)I{Entity}QueryRepository.ts- For query operations (read-only, optimized for specific views)
- Example:
ICardRepository.ts,ICardQueryRepository.ts,ICollectionRepository.ts,ICollectionQueryRepository.ts
2. Application Layer #
Use Cases #
- Location:
src/modules/{module}/application/useCases/ - Structure:
commands/- For state-changing operationsqueries/- For read-only operations
Command Use Case Pattern #
// Example: AddUrlToLibraryUseCase
export interface AddUrlToLibraryDTO {
url: string;
note?: string;
collectionIds?: string[];
curatorId: string;
}
export class AddUrlToLibraryUseCase
implements UseCase<AddUrlToLibraryDTO, Result<ResponseDTO>>
{
constructor(
private cardRepository: ICardRepository,
private metadataService: IMetadataService,
private cardLibraryService: CardLibraryService,
private cardCollectionService: CardCollectionService,
) {}
async execute(request: AddUrlToLibraryDTO): Promise<Result<ResponseDTO>> {
// 1. Validate input
// 2. Create/fetch domain entities
// 3. Apply business rules via domain services
// 4. Persist changes
// 5. Return result
}
}
Query Use Case Pattern #
// Example: GetCollectionPageUseCase
export interface GetCollectionPageQuery {
collectionId: string;
page?: number;
limit?: number;
sortBy?: CardSortField;
sortOrder?: SortOrder;
}
export class GetCollectionPageUseCase
implements UseCase<GetCollectionPageQuery, Result<GetCollectionPageResult>>
{
constructor(
private collectionRepo: ICollectionRepository,
private cardQueryRepo: ICardQueryRepository,
private profileService: IProfileService,
) {}
async execute(
query: GetCollectionPageQuery,
): Promise<Result<GetCollectionPageResult>> {
// 1. Validate query parameters
// 2. Fetch data from query repositories
// 3. Aggregate and transform data
// 4. Return structured result
}
}
3. Infrastructure Layer #
Repository Implementations #
- Location:
src/modules/{module}/infrastructure/repositories/ - Files:
Drizzle{Entity}Repository.ts- Implements command repository interfaceDrizzle{Entity}QueryRepository.ts- Implements query repository interface with optimized read operations
- Purpose: Implement domain repository interfaces with specific technology (Drizzle ORM)
- Pattern: Query repositories often return DTOs optimized for specific views, while command repositories work with full domain entities
HTTP Controllers #
- Location:
src/modules/{module}/infrastructure/http/controllers/ - Pattern:
export class {Feature}Controller extends Controller {
constructor(private {feature}UseCase: {Feature}UseCase) {
super();
}
async executeImpl(req: AuthenticatedRequest, res: Response): Promise<any> {
try {
// 1. Extract and validate request data
// 2. Call use case
// 3. Handle result and return appropriate HTTP response
} catch (error) {
return this.fail(res, error);
}
}
}
Routes #
- Location:
src/modules/{module}/infrastructure/http/routes/ - Purpose: Define HTTP endpoints and wire controllers
- Pattern: Group related endpoints, apply middleware (auth, validation)
4. Dependency Injection & Factories #
Factory Registration #
All new components must be registered in the appropriate factories:
-
RepositoryFactory (
src/shared/infrastructure/http/factories/RepositoryFactory.ts)- Register both command and query repository implementations
-
ServiceFactory (
src/shared/infrastructure/http/factories/ServiceFactory.ts)- Register domain services and external services
-
UseCaseFactory (
src/shared/infrastructure/http/factories/UseCaseFactory.ts)- Register use cases with their dependencies
-
ControllerFactory (
src/shared/infrastructure/http/factories/ControllerFactory.ts)- Register controllers with their use cases
5. API Client Layer #
Client Structure #
- Location:
src/webapp/api-client/ - Files:
types/requests.ts- Request DTOstypes/responses.ts- Response DTOsclients/{Module}Client.ts- HTTP client implementationsApiClient.ts- Main client facade
Client Pattern #
export class {Module}Client extends BaseClient {
async {operation}(request: {Operation}Request): Promise<{Operation}Response> {
return this.request<{Operation}Response>(
'POST', // or appropriate HTTP method
'/api/{endpoint}',
request,
);
}
}
Implementation Checklist #
When implementing a new feature, follow this checklist:
Domain Layer #
Application Layer #
Infrastructure Layer #
API Client Layer #
Integration #
Repository Pattern (CQRS) #
- Command Repositories: Handle write operations, work with full domain entities, enforce business rules
- Query Repositories: Handle read operations, return optimized DTOs, support pagination and sorting
- Separation: Commands use
I{Entity}Repository, queries useI{Entity}QueryRepository - DTOs: Query repositories return view-specific DTOs (e.g.,
UrlCardView,CollectionQueryResultDTO)
Key Patterns & Conventions #
Error Handling #
- Use
Result<T, E>pattern for use cases - Define specific error types that extend
UseCaseError - Controllers handle errors and return appropriate HTTP status codes
Validation #
- Input validation in use cases using value objects
- Domain validation in entities and value objects
- HTTP validation in controllers
Authentication #
- Use
AuthenticatedRequestfor protected endpoints - Extract user identity (
did) from request - Pass user context to use cases
Pagination & Sorting #
- Standardize pagination parameters (
page,limit) - Use enums for sort fields and order
- Return pagination metadata in responses
Testing Strategy #
- Unit tests for domain logic
- Integration tests for use cases
- In-memory implementations for testing
- Mock external services
Common Pitfalls to Avoid #
- Mixing Concerns: Keep domain logic in domain layer, not in controllers or repositories
- Anemic Domain Model: Ensure entities have behavior, not just data
- Repository Leakage: Don't expose ORM-specific types in domain interfaces
- Missing Error Handling: Always handle and propagate errors appropriately
- Inconsistent Patterns: Follow established patterns for similar operations
Example Files to Reference #
Command Example #
- Use Case:
src/modules/cards/application/useCases/commands/AddUrlToLibraryUseCase.ts - Controller:
src/modules/cards/infrastructure/http/controllers/AddUrlToLibraryController.ts
Query Example #
- Use Case:
src/modules/cards/application/useCases/queries/GetCollectionPageUseCase.ts - Controller:
src/modules/cards/infrastructure/http/controllers/GetCollectionPageController.ts
Repository Examples #
- Command Repository Interface:
src/modules/cards/domain/ICardRepository.ts - Query Repository Interface:
src/modules/cards/domain/ICardQueryRepository.ts - Command Repository Implementation:
src/modules/cards/infrastructure/repositories/DrizzleCardRepository.ts - Query Repository Implementation:
src/modules/cards/infrastructure/repositories/DrizzleCardQueryRepository.ts
Factory Examples #
- All factories in
src/shared/infrastructure/http/factories/
This guide should be used as a reference when implementing new features to ensure consistency with the established architecture and patterns.
example context: #
docs/features/GUIDE.md
src/modules/cards/application/useCases/commands/AddUrlToLibraryUseCase.ts
src/modules/cards/domain/ICardQueryRepository.ts
src/modules/cards/domain/ICardRepository.ts
src/modules/cards/infrastructure/http/controllers/AddUrlToLibraryController.ts
src/modules/cards/infrastructure/http/routes/cardRoutes.ts
src/modules/cards/infrastructure/http/routes/collectionRoutes.ts
src/modules/user/infrastructure/http/routes/userRoutes.ts
src/shared/domain/AggregateRoot.ts
src/shared/domain/events/IDomainEvent.ts
src/shared/infrastructure/http/app.ts
src/shared/infrastructure/http/factories/ControllerFactory.ts
src/shared/infrastructure/http/factories/RepositoryFactory.ts
src/shared/infrastructure/http/factories/ServiceFactory.ts
src/shared/infrastructure/http/factories/UseCaseFactory.ts
src/modules/cards/domain/Card.ts
src/modules/cards/domain/events/CardAddedToLibraryEvent.ts
src/modules/cards/infrastructure/http/routes/index.ts