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#
- Create/update domain entities if needed
- Create/update value objects if needed
- Define command repository interfaces (for write operations)
- Define query repository interfaces (for read operations with specific DTOs)
- Implement domain services for complex business logic
Application Layer#
- Create use case (command or query)
- Define request/response DTOs
- Implement business logic and validation
- Handle error cases appropriately
Infrastructure Layer#
- Implement command repository (if new entity)
- Implement query repository (if new entity)
- Create HTTP controller
- Define routes
- Register in factories
API Client Layer#
- Define request/response types
- Implement client methods
- Update main ApiClient facade
Integration#
- Register all components in factories
- Wire routes in main app
- Test end-to-end flow
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