Skip to main content

ADR 0001: Standardized Service Layout

Date: 2025-08-05

Status: Accepted

Context

As we build out the Citadel platform using a polyglot monorepo, we need a consistent and predictable directory structure for all backend services, regardless of language. A standardized layout is crucial for:

  • Reducing Cognitive Load: Developers can navigate any service and immediately understand where to find different types of logic.
  • Improving Developer Experience: Consistency simplifies tooling, scripting, and automation.
  • Enforcing Architectural Principles: The directory structure itself can guide developers to maintain a clean separation of concerns (e.g., separating domain logic from infrastructure details).

We need a structure that explicitly supports the principles of Domain-Driven Design (DDD) and Clean/Hexagonal Architecture. The specific implementation will be adapted to the conventions of each language (Go, Python, TypeScript), but the layered principle remains the same.

Decision

We will adopt a standardized, four-layered architectural layout for all backend services within the apps/ directory. This structure is designed to enforce separation of concerns and clearly map to DDD concepts.

The following sections provide concrete examples for both Python, NestJS and Go. While language conventions differ, the core layered philosophy remains identical. base directory can be app or any meaningful name the runtime allows. e.g. book_keeper.

Python Service Structure Example

/
├── app/
│ ├── api/
│ │ ├── ${version}/
│ │ │ ├── dtos/
│ │ │ │ └── ${dto_name}.py
│ │ │ ├── routers/
│ │ │ │ └── ${router_name}.py
│ │ │ └── __init__.py
│ │ ├── deps.py
│ │ ├── exceptions.py
│ │ └── __init__.py
│ ├── application/
│ │ ├── ${bounded_ctx}/
│ │ │ ├── commands/
│ │ │ │ └── ${command_name}.py
│ │ │ ├── handlers/
│ │ │ │ ├── ${command_handler_name}.py
│ │ │ │ ├── ${query_handler_name}.py
│ │ │ │ ├── ${event_handler_name}.py
│ │ │ │ └── ${handler_permission_name}.py
│ │ │ ├── projectors/
│ │ │ │ └── ${application_projector_name}.py
│ │ │ ├── queries/
│ │ │ │ └── ${query_name}.py
│ │ │ ├── __init__.py
│ │ │ └── di.py
│ │ ├── exceptions.py
│ │ └── __init__.py
│ ├── domain/
│ │ ├── ${bounded_ctx}/
│ │ │ ├── aggregates/
│ │ │ │ └── ${domain_aggregate_name}.py
│ │ │ ├── event_sourced_aggregates/
│ │ │ │ └── ${domain_event_sourced_aggregate_name}.py
│ │ │ ├── events/
│ │ │ │ └── ${domain_event_name}.py
│ │ │ ├── factories/
│ │ │ │ └── ${factory_name}.py
│ │ │ ├── policies/
│ │ │ │ └── ${domain_business_policy_name}.py
│ │ │ ├── projections/
│ │ │ │ └── ${domain_projection_name}.py
│ │ │ ├── repositories/ (Interfaces)
│ │ │ │ ├── ${domain_repository_name}.py
│ │ │ │ └── ${domain_event_sourced_repository_name}.py
│ │ │ ├── sagas/
│ │ │ │ ├── ${saga_orchestrator_name}.py
│ │ │ │ └── ${saga_state_name}.py
│ │ │ ├── services/
│ │ │ │ └── ${domain_service_name}.py
│ │ │ ├── specifications/
│ │ │ │ └── ${domain_specification_name}.py
│ │ │ ├── value_objects/
│ │ │ │ └── ${domain_value_object_name}.py
│ │ │ ├── __init__.py
│ │ │ ├── di.py
│ │ │ └── exceptions.py
│ │ ├── shared_kernel/
│ │ │ └── ... (similar structure to a bounded context)
│ │ └── __init__.py
│ ├── infrastructure/
│ │ ├── event_stores/
│ │ │ └── ${infrastructure_event_store_name}.py
│ │ ├── messaging/
│ │ │ ├── ${event_consumer_name}.py
│ │ │ ├── ${event_publisher_name}.py
│ │ │ └── __init__.py
│ │ ├── persistence/
│ │ │ ├── ${bounded_ctx}/
│ │ │ │ ├── anti_corruption/
│ │ │ │ │ └── translators/
│ │ │ │ │ └── ${anti_corruption_translator_name}.py
│ │ │ │ ├── models/
│ │ │ │ │ └── ${model_name}.py
│ │ │ │ └── repositories/ (Implementations)
│ │ │ │ ├── ${infrastructure_repository_name}.py
│ │ │ │ ├── ${saga_state_model_name}.py
│ │ │ │ └── ${saga_state_repository_name}.py
│ │ │ └── __init__.py
│ │ ├── projection_stores/
│ │ │ └── ${infrastructure_projection_store_name}.py
│ │ ├── snapshot_stores/
│ │ │ └── ${infrastructure_snapshot_store_name}.py
│ │ ├── ${infrastructure_module_name}/
│ │ │ └── ${infrastructure_service_name}.py
│ │ ├── __init__.py
│ │ ├── di.py
│ │ └── exceptions.py
│ ├── __init__.py
│ ├── config.py
│ ├── di.py
│ └── main.py
└── migrations/

Go Service Structure Example

/
├── app/
│ ├── cmd/
│ │ ├── root.go
│ │ └── serve.go
│ └── main.go
└── internal/
├── app/
│ ├── config.go
│ └── container.go
├── api/
│ ├── ${version}/
│ │ ├── dtos/
│ │ └── routers/
│ ├── deps.go
│ └── exceptions.go
├── application/
│ ├── ${bounded_ctx}/
│ │ ├── commands/
│ │ ├── handlers/
│ │ ├── projectors/
│ │ └── queries/
│ └── exceptions.go
├── domain/
│ ├── ${bounded_ctx}/
│ │ ├── aggregates/
│ │ ├── events/
│ │ ├── services/
│ │ └── repositories/ (Interfaces)
│ └── shared_kernel/
└── infrastructure/
├── messaging/
└── persistence/
└── ${bounded_ctx}/
├── models/
└── repositories/ (Implementations)

NestJS Service Structure Example

/
└── app/
├── main.ts
├── app.module.ts
├── config/
│ └── index.ts
├── api/
│ ├── v1/
│ │ ├── dtos/
│ │ │ └── create-something.dto.ts
│ │ ├── create-something.controller.ts
│ │ └── api.module.ts
│ └── ...
├── application/
│ ├── commands/
│ │ ├── impl/
│ │ │ └── create-something.command.ts
│ │ └── handlers/
│ │ └── create-something.handler.ts
│ ├── queries/
│ │ ├── impl/
│ │ │ └── get-something.query.ts
│ │ └── handlers/
│ │ └── get-something.handler.ts
│ └── application.module.ts
├── domain/
│ ├── aggregates/
│ │ └── something.aggregate.ts
│ ├── events/
│ │ └── something-created.event.ts
│ ├── repositories/
│ │ └── something.repository.ts
│ └── domain.module.ts
└── infrastructure/
├── persistence/
│ ├── typeorm/
│ │ ├── entities/
│ │ │ └── something.entity.ts
│ │ └── repositories/
│ │ └── something.repository.impl.ts
│ └── persistence.module.ts
├── messaging/
│ ├── nats-publisher.service.ts
│ └── messaging.module.ts
└── infrastructure.module.ts

Layer Responsibilities

  1. domain Layer:

    • Purpose: The heart of the service. Contains all core business logic, rules, and types. It is completely independent of any other layer.
    • Contents: Aggregates, Entities, Value Objects, Domain Events, and Repository Interfaces. It knows nothing about databases, APIs, or message queues.
  2. application Layer:

    • Purpose: Orchestrates the domain layer to perform tasks required by the user. It defines the application's use cases.
    • Contents: Command and Query objects, and their corresponding Handlers. It uses the repository interfaces defined in the domain layer to fetch and persist domain objects. It does not contain business logic.
  3. api Layer (or other delivery mechanisms):

    • Purpose: The entry point to the application layer. It handles incoming requests, translates them into application commands or queries, and returns responses.
    • Contents: HTTP routers, controllers, Data Transfer Objects (DTOs), and API-specific exception handling.
  4. infrastructure Layer:

    • Purpose: Contains the concrete implementations of the interfaces defined in the domain and application layers. It is the layer where all external dependencies are managed.
    • Contents: Database repository implementations, message bus publishers/consumers, clients for external services, and other "details" that the core application depends on.

Consequences

Positive

  • Clear Separation of Concerns: The structure makes it difficult to accidentally mix business logic with infrastructure code.
  • High Testability: The domain and application layers can be tested with fast unit tests without needing to spin up databases or web servers.
  • Flexibility: Since the core logic is independent, we can easily change infrastructure details (e.g., swap PostgreSQL for a different database) by simply creating a new implementation of a repository interface.
  • Scalability: The clear boundaries make it easier for multiple developers to work on the same service without conflicts.

Negative

  • Increased Boilerplate: For very simple CRUD services, this structure can feel overly complex and lead to a higher number of files. This is a deliberate trade-off we accept for the long-term maintainability and consistency of the entire platform.

This ADR formalizes the PROJECT_STRUCTURE_DEFINITION provided.