Skip to content

Latest commit

 

History

History
81 lines (64 loc) · 3.64 KB

File metadata and controls

81 lines (64 loc) · 3.64 KB
status accepted
date 2026-07-16
decision-makers Paul Custance

Structure the solution using clean architecture

Context and Problem Statement

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?

Decision Drivers

  • 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.

Considered Options

  • 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

Decision Outcome

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 on SchoolAccount.Collect.SharedKernel.
  • SchoolAccount.Collect.Application - command/query handlers for each use case, implementing ICommandHandler/IQueryHandler interfaces. Handlers are decorated with cross-cutting concerns such as validation and logging. Depends on SchoolAccount.Collect.Domain and SchoolAccount.Collect.SharedKernel.
  • SchoolAccount.Collect.Infrastructure - EF Core/PostgreSQL persistence, authentication, time - implementations of abstractions the inner layers define; depends on SchoolAccount.Collect.Application.
  • SchoolAccount.Collect.Api - minimal API endpoints and composition root; references SchoolAccount.Collect.Infrastructure only to wire dependency injection.

Consequences

  • 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.

Confirmation

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.

More Information