Clean Architecture
This document explains how Clean Architecture is applied across all projects following these standards. Every project follows this structure regardless of domain complexity.
Agent Quick Rules {#agent-quick-rules}
- Dependencies MUST point inward: Domain has no infrastructure references.
- WebApi references Contracts projects only, not Write/Read implementation projects.
- Command handlers live in
Application.Write; query handlers inApplication.Read. - Query handlers MUST use
IDatabaseContext; MUST NOT inject repository interfaces. - Reactions handle domain events with narrow side-effect interfaces only.
- Infrastructure implements repositories, EF Core, pipeline, and external clients.
- Endpoints use
IEndpoint; MUST NOT use MVC controllers.
Full convention: docs/architecture/clean-architecture.md
Key decisions documented in
docs/decisions/:docs/decisions/clean-architecture-as-structural-foundation.md(Clean Architecture as foundation),docs/decisions/cqrs-with-split-application-projects.md(CQRS project split),docs/decisions/minimal-api-endpoint-classes.md(IEndpoint pattern),docs/decisions/idatabasecontext-over-per-aggregate-read-stores.md(IDatabaseContext),docs/decisions/transaction-pipeline-behaviors.md(Transaction pipeline behaviors).
1. Layer Diagram
graph TD WebApi["WebApi\n(Endpoints, Request/Response Models, ApiMappings)"] WriteContracts["Application.Write.Contracts\n(Commands, Command Results, CommandValidationException)"] ReadContracts["Application.Read.Contracts\n(Queries, Query Results, IDatabaseContext, PagedResult<T>, QueryValidationException)"] Write["Application.Write\n(Command Handlers, Validators)"] Read["Application.Read\n(Query Handlers, Validators)\nReferences Microsoft.EntityFrameworkCore"] Reactions["Application.Reactions\n(Event Handlers, Narrow Interfaces)"] Domain["Domain\n(Aggregates, Value Objects, Events, Repository Interfaces)"] Infrastructure["Infrastructure\n(EF Core, Repositories, AppDbContext implements IDatabaseContext,\nTransaction Pipeline Behaviors, External Clients)"]
WebApi --> WriteContracts WebApi --> ReadContracts Write --> WriteContracts Write --> Domain Read --> ReadContracts Read --> Domain Reactions --> WriteContracts Reactions --> ReadContracts Reactions --> Domain Infrastructure --> Domain Infrastructure --> WriteContracts Infrastructure --> ReadContracts Infrastructure --> Reactions2. Layer Responsibilities
Domain
The Domain layer contains the core business model. It has zero dependencies on any other project in the solution and no dependency on persistence, web, UI, or messaging frameworks. It may use the .NET BCL and the approved guard clause package. Its aggregate shape is deliberately compatible with EF Core materialisation, but the Domain project does not reference EF Core.
Contains:
- Aggregate roots and their child entities
- Value objects
- Domain events
- Domain exceptions (
DomainExceptionsubclasses,AggregateNotFoundExceptionsubclasses) - Repository interfaces (
IPostRepository,ICustomerRepository, etc.) - Strongly-typed IDs (
PostId,CustomerId, etc.)
Forbidden:
- Any reference to
Microsoft.EntityFrameworkCore - Any reference to
Microsoft.AspNetCore.* - Application models, DTOs, or read projection types
- Infrastructure concerns: connection strings, HTTP clients, file paths
Private parameterless constructors and private collection backing fields are permitted in aggregate roots even though they exist primarily for EF Core materialisation compatibility. The compatibility is structural, not a package dependency.
Application.Write.Contracts
The public contract of the write path. Contains only types that form the API surface of write operations.
Contains:
- Command record types (
CreatePostCommand,PublishPostCommand) - Command result record types (
PostIdas a result, or dedicated result records) - Write-side contract interfaces that Infrastructure implements
- NuGet reference:
LiteBus.Commands.Abstractions
Forbidden:
- Command handler implementations
- Command validator implementations
- Any business logic
Application.Write
The private implementation of the write path.
Contains:
- Command handler implementations (
ICommandHandler<TCommand>,ICommandHandler<TCommand, TResult>) - Command validator implementations (
ICommandValidator<TCommand>) - References:
Application.Write.Contracts,Domain - NuGet reference:
LiteBus.Commands.Abstractions
Forbidden:
- Command or query type definitions (those belong in Contracts)
- Read store interfaces or query types
- Any reference to
Microsoft.EntityFrameworkCore
Application.Read.Contracts
The public contract of the read path. Contains only types that form the API surface of read operations.
Contains:
- Query record types (
GetPostByIdQuery) - Query result record types (
PostResult,PostSummary) IDatabaseContextinterface exposingIQueryable<T>per aggregatePagedResult<T>andPaginationParametersshared pagination typesQueryValidationExceptionbase class- NuGet reference:
LiteBus.Queries.Abstractions
Forbidden:
- Query handler implementations
- Query validator implementations
- Any business logic
Application.Read
The private implementation of the read path.
Contains:
- Query handler implementations (
IQueryHandler<TQuery, TResult>) - Query validator implementations (
IQueryValidator<TQuery>) - References:
Application.Read.Contracts,Domain,Microsoft.EntityFrameworkCore - NuGet reference:
LiteBus.Queries.Abstractions - Injects
IDatabaseContextfor all data access
Forbidden:
- Query or result type definitions (those belong in Contracts)
- Any reference to the Infrastructure project directly (use
IDatabaseContextfromApplication.Read.Contracts)
Application.Reactions
Event handler implementations that react to domain events.
Contains:
- Event handler implementations (
IEventHandler<TEvent>) - Narrow, per-handler interfaces for external side effects (
IPostPublishedNotifier) - References:
Application.Write.Contracts,Application.Read.Contracts,Domain - NuGet references:
LiteBus.Events.Abstractions;LiteBus.Commands.Abstractionsonly when a handler dispatches follow-up commands
Forbidden:
- Any reference to external library packages (EF Core, email clients, HTTP clients)
- Any business rule enforcement (that belongs in Domain)
Infrastructure
Adapts external systems to the interfaces defined by Domain and Application.
Contains:
- EF Core
DbContextimplementation (AppDbContextimplementsIDatabaseContext) IEntityTypeConfiguration<T>classes for all aggregatesIXxxRepositoryimplementations- Global LiteBus pipeline behaviors:
TransactionCommandPreHandler,SaveChangesCommandPostHandler,RollbackCommandErrorHandler - Implementations of narrow interfaces defined in
Application.Reactions - External service clients (email, payment, blob storage)
- EF Core migrations
- DI registration extension methods
Forbidden:
- Business logic or domain rule enforcement
- Knowledge of the HTTP request lifecycle
- References to
Microsoft.AspNetCore.*except for DI types
WebApi
The HTTP adapter. Translates HTTP requests into application commands and queries.
Contains:
IEndpointimplementations (one class per use case)- Request and response record types
ApiMappingsextension classesGlobalExceptionHandlermiddleware- OpenAPI configuration
Forbidden:
- Business logic of any kind
- Direct access to repositories,
IDatabaseContext, orDbContext try-catchblocks (handled byGlobalExceptionHandler)- Domain types in response models
3. Dependency Rule
Dependencies point inward only. No inner layer may reference an outer layer.
graph LR Domain["Domain"] WriteContracts["App.Write.Contracts"] ReadContracts["App.Read.Contracts"] Write["App.Write"] Read["App.Read"] Reactions["App.Reactions"] Infra["Infrastructure"] WebApi["WebApi"]
WriteContracts --> Domain ReadContracts --> Domain Write --> WriteContracts Write --> Domain Read --> ReadContracts Read --> Domain Reactions --> WriteContracts Reactions --> ReadContracts Reactions --> Domain Infra --> Domain Infra --> WriteContracts Infra --> ReadContracts Infra --> Reactions WebApi --> WriteContracts WebApi --> ReadContracts WebApi -.->|"Program.cs only"| Infra WebApi -.->|"Program.cs only"| Write WebApi -.->|"Program.cs only"| Read WebApi -.->|"Program.cs only"| Reactions| Project | May Reference |
|---|---|
Domain | Nothing |
Application.Write.Contracts | Domain |
Application.Read.Contracts | Domain |
Application.Write | Application.Write.Contracts, Domain |
Application.Read | Application.Read.Contracts, Domain, Microsoft.EntityFrameworkCore |
Application.Reactions | Application.Write.Contracts, Application.Read.Contracts, Domain |
Infrastructure | Domain, Application.Write.Contracts, Application.Read.Contracts, Application.Reactions |
WebApi | Application.Write.Contracts, Application.Read.Contracts |
WebApi (Program.cs only) | Infrastructure, Application.Write, Application.Read, Application.Reactions for DI registration |
4. CQRS Split
Commands and queries are handled by separate classes in separate projects. This enforces a hard split between the read path and the write path at the compiler level.
Commands modify state. A command handler:
- Validates input (a separate validator runs before the handler via LiteBus pipeline).
- Loads the aggregate from its repository.
- Calls a method on the aggregate that enforces the business rule.
- Saves the aggregate back via the repository.
- Returns void or a simple creation result.
Queries return data. A query handler:
- Validates input (a separate validator runs before the handler).
- Writes a LINQ projection against
IDatabaseContext. - Returns the projection result or throws an
AggregateNotFoundExceptionsubclass if not found.
Query handlers MUST NOT load domain aggregates. They MUST NOT inject repository interfaces. See docs/conventions/backend/query-read-strategy.md for full details.
5. LiteBus as Mediator
LiteBus is a modular mediator. Each project references only the package it needs, not the full metapackage. This keeps project dependencies minimal and explicit.
| LiteBus Package | Project | Purpose |
|---|---|---|
LiteBus.Commands.Abstractions | Application.Write.Contracts, Application.Write, Application.Reactions | ICommand, ICommandHandler, ICommandValidator, ICommandMediator |
LiteBus.Queries.Abstractions | Application.Read.Contracts, Application.Read | IQuery, IQueryHandler, IQueryValidator, IQueryMediator |
LiteBus.Events.Abstractions | Application.Reactions | IEvent, IEventHandler, IEventPublisher |
LiteBus.Extensions.Microsoft.DependencyInjection | WebApi | Full DI registration |
LiteBus.Commands.Abstractions | Infrastructure | ICommandPreHandler<ICommand>, ICommandPostHandler<ICommand>, ICommandErrorHandler<ICommand> for global pipeline behaviors |
Endpoints dispatch via ICommandMediator or IQueryMediator. These interfaces are the specific entry points for endpoints. WebApi does not reference the handler implementation projects directly in endpoint code; it references only the Contracts projects for the command and query types.
// GOOD: endpoint dispatches via specific mediator, references only Contracts typessealed class CreatePostEndpoint : IEndpoint{ public void MapEndpoint(IEndpointRouteBuilder app) { app.MapPost("/posts", HandleAsync); }
private static async Task<IResult> HandleAsync( CreatePostRequest request, ICommandMediator commandMediator, CancellationToken cancellationToken) { var command = request.ToCommand(); var postId = await commandMediator.SendAsync(command, cancellationToken); return Results.Created($"/posts/{postId.Value}", postId.ToResponse()); }}6. Event Handling and the Reactions Project
Domain events originate in aggregates. An aggregate method raises a domain event by calling RaiseDomainEvent(new PostPublished(Id)). Infrastructure collects these events during the command pipeline and dispatches them after the aggregate changes are committed, or writes them to the outbox when durable delivery is required.
The Application.Reactions project contains event handlers. Each handler reacts to one domain event. There are two default categories:
- Dispatches a follow-up command: The handler receives an event and sends a command via
ICommandMediator. Use this only when eventual consistency is acceptable and the command is idempotent. - Triggers an external side effect through a narrow interface: The handler receives an event and calls a project-owned interface such as
IPostPublishedNotifier.
Read model projection updates are not a default Reactions category. Query handlers read through IDatabaseContext projections by default. If a project needs denormalized read model tables, document the projection ownership in a project ADR and use the outbox pattern when stale projections are not acceptable.
The Reactions project MUST NOT reference external libraries. All external capabilities are accessed through narrow interfaces defined in the Reactions project and implemented by Infrastructure.
// GOOD: narrow interface defined in Application.Reactionsinternal interface IPostPublishedNotifier{ Task NotifySubscribersAsync(PostId postId, string postTitle, CancellationToken cancellationToken);}
// GOOD: event handler uses the narrow interfaceinternal sealed class NotifySubscribersOnPostPublishedEventHandler : IEventHandler<PostPublished>{ private readonly IPostPublishedNotifier _notifier;
public NotifySubscribersOnPostPublishedEventHandler( IPostPublishedNotifier notifier) { _notifier = notifier; }
public async Task HandleAsync(PostPublished @event, CancellationToken cancellationToken) { await _notifier.NotifySubscribersAsync( @event.PostId, @event.PostTitle, cancellationToken); }}
// BAD: event handler references an external library directlyinternal sealed class NotifySubscribersOnPostPublishedEventHandler : IEventHandler<PostPublished>{ private readonly IEmailClient _emailClient; // BAD: external library in Application.Reactions}7. Transaction Pipeline Behaviors
Transaction management is handled by three global LiteBus pipeline behaviors registered in Infrastructure. Command handlers do not call SaveChangesAsync. Repositories do not call SaveChangesAsync. The pipeline handles all persistence.
The execution order for every command:
sequenceDiagram participant Mediator participant Validator as ICommandValidator<T><br/>(priority 0) participant TxPre as TransactionCommandPreHandler<br/>(priority 10) participant Handler as ICommandHandler<T> participant TxPost as SaveChangesCommandPostHandler participant ErrorH as RollbackCommandErrorHandler
Mediator->>Validator: PreHandleAsync (validate input) alt Validation fails Validator-->>Mediator: throws CommandValidationException end Mediator->>TxPre: PreHandleAsync (begin transaction) Mediator->>Handler: HandleAsync (business logic) alt Handler throws Handler-->>ErrorH: exception ErrorH->>ErrorH: RollbackTransactionAsync ErrorH-->>Mediator: re-throws end Mediator->>TxPost: PostHandleAsync (SaveChanges + Commit)Handler Priority Scheme
| Priority | Handler | Scope |
|---|---|---|
| 0 (default) | ICommandValidator<TCommand> | Specific per command; runs before the transaction opens |
| 10 | TransactionCommandPreHandler | Global for all ICommand; opens transaction after validation |
Implementation
Full handler implementations, assembly marker classes, and LiteBus registration live in docs/conventions/backend/infrastructure-layer.md (Transaction Pipeline Behaviors and LiteBus Registration sections). This document describes the pattern; the Infrastructure convention is authoritative for code.
LiteBus Registration
Register LiteBus in WebApi/Program.cs only. See docs/blueprints/backend/program-cs.md for the complete composition root. Do not duplicate registration in other convention files.
Handler classes are internal sealed. Each implementation project exposes a public static class {Layer}AssemblyMarker { } so Program.cs can reference the assembly without importing internal types. See docs/conventions/backend/infrastructure-layer.md for assembly marker examples.
8. Architecture Tests
Structural rules are enforced by architecture tests using NetArchTest in addition to project reference constraints. Project references prevent the most obvious violations. Architecture tests catch violations that project references cannot.
Architecture tests live in {ProjectName}.Architecture.Tests. They run in CI on every PR. See docs/conventions/backend/testing.md for full examples.
Three concrete examples of rules that architecture tests enforce:
-
Query handlers must not depend on repository interfaces. The project reference alone does not prevent this if a future refactoring introduces a shared project containing both. The test explicitly asserts no such dependency exists.
-
Handlers must be
internal sealed. Handlers are implementation details and must not bepublic. The test asserts this for all types implementingICommandHandler<,>orIQueryHandler<,>. -
Reactions must not reference external libraries. NetArchTest checks that no type in the Reactions assembly has a dependency on
Microsoft.EntityFrameworkCoreor similar packages.
9. The AggregateRoot Base Class
Every project defines three types in Domain/Shared/. They are not provided by a NuGet package; they are owned by the project.
/// <summary>/// Non-generic marker interface implemented by all aggregate roots./// Used by Infrastructure to query the EF Core change tracker without/// knowing the concrete ID type./// </summary>public interface IAggregateRoot{ IReadOnlyList<IDomainEvent> DomainEvents { get; } void ClearDomainEvents();}/// <summary>/// The base class for all aggregate roots. Provides domain event collection/// and the strongly-typed ID contract./// </summary>abstract class AggregateRoot<TId> : IAggregateRoot where TId : struct{ private readonly List<IDomainEvent> _domainEvents = [];
/// <summary> /// The unique identifier of this aggregate root. /// </summary> public TId Id { get; protected set; } // set, not init — required for EF Core materialisation
/// <summary> /// Domain events raised during this aggregate's lifetime, dispatched /// after the transaction commits. /// </summary> public IReadOnlyList<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();
/// <summary> /// Records a domain event to be dispatched after the transaction commits. /// </summary> protected void RaiseDomainEvent(IDomainEvent domainEvent) { _domainEvents.Add(domainEvent); }
/// <summary> /// Clears all recorded domain events. Called by Infrastructure after /// events have been dispatched. /// </summary> public void ClearDomainEvents() { _domainEvents.Clear(); }}The IDomainEvent marker interface and visibility rules are defined in docs/conventions/backend/domain-layer.md (Communication via Domain Events). That document is authoritative for event naming, structure, and the public visibility requirement.
All three types live in Domain/Shared/. All aggregate roots extend AggregateRoot<TId>. All domain event records implement IDomainEvent. Infrastructure calls ClearDomainEvents() after dispatching events via LiteBus.