A social knowledge tool for researchers built on ATProto

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 operations
    • queries/ - 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 interface
    • Drizzle{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:

  1. RepositoryFactory (src/shared/infrastructure/http/factories/RepositoryFactory.ts)

    • Register both command and query repository implementations
  2. ServiceFactory (src/shared/infrastructure/http/factories/ServiceFactory.ts)

    • Register domain services and external services
  3. UseCaseFactory (src/shared/infrastructure/http/factories/UseCaseFactory.ts)

    • Register use cases with their dependencies
  4. 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 DTOs
    • types/responses.ts - Response DTOs
    • clients/{Module}Client.ts - HTTP client implementations
    • ApiClient.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 use I{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 AuthenticatedRequest for 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#

  1. Mixing Concerns: Keep domain logic in domain layer, not in controllers or repositories
  2. Anemic Domain Model: Ensure entities have behavior, not just data
  3. Repository Leakage: Don't expose ORM-specific types in domain interfaces
  4. Missing Error Handling: Always handle and propagate errors appropriately
  5. 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