| status | accepted |
|---|---|
| date | 2026-07-16 |
| decision-makers | Paul Custance |
APIs built from this template will grow domain logic that outlives their initial CRUD endpoints, and that logic must stay testable and independent of frameworks and infrastructure. How should the solution be structured so that business rules do not become entangled with HTTP, persistence, and other concerns that change for different reasons?
- Domain and application logic must be unit-testable without a database, web host, or other infrastructure.
- Infrastructure choices (ORM, database, auth provider) should be replaceable without rewriting business rules.
- The structure must be enforceable automatically, not just by convention, template consumers will not read a style guide.
- Newcomers should find one obvious place for each kind of code.
- Clean architecture with one project per layer
- Traditional N-tier layering (UI -> business logic -> data access)
- Vertical slice architecture in a single project
- Single-project minimal API with no enforced layering
Chosen option: "Clean architecture with one project per layer", because it is the only option that both inverts dependencies away from infrastructure and can be enforced mechanically at the assembly level.
The solution is split into five projects, with all dependencies pointing inward:
SchoolAccount.Collect.SharedKernel- base building blocks (Result,Error) with no dependencies.SchoolAccount.Collect.Domain- entities and business rules; depends only onSchoolAccount.Collect.SharedKernel.SchoolAccount.Collect.Application- command/query handlers for each use case, implementingICommandHandler/IQueryHandlerinterfaces. Handlers are decorated with cross-cutting concerns such as validation and logging. Depends onSchoolAccount.Collect.DomainandSchoolAccount.Collect.SharedKernel.SchoolAccount.Collect.Infrastructure- EF Core/PostgreSQL persistence, authentication, time - implementations of abstractions the inner layers define; depends onSchoolAccount.Collect.Application.SchoolAccount.Collect.Api- minimal API endpoints and composition root; referencesSchoolAccount.Collect.Infrastructureonly to wire dependency injection.
- Good, because handlers and domain types are testable in isolation; the architecture tests run with no infrastructure at all.
- Good, because layer violations fail the build rather than accumulating silently.
- Bad, because simple features carry ceremony: one endpoint typically means an endpoint class, a query/command, a handler, and a response type across two projects.
- Bad, because five projects is a heavier starting point than a single-project API for genuinely small services.
tests/SchoolAccount.Collect.ArchitectureTests encodes the dependency rules with NetArchTest (e.g.
SchoolAccount.Collect.Domain must not depend on SchoolAccount.Collect.Application,
SchoolAccount.Collect.Application must not depend on SchoolAccount.Collect.Infrastructure). These tests run in the
Build workflow on every pull request, so a violating change cannot merge cleanly.
- The Clean Architecture - Robert C. Martin's original description of the dependency rule.
- NetArchTest - the library used by the architecture tests.
- Related: ADR-0001 established this decision log.