.NET Architecture Best Practices
Apply these practices when designing, building, reviewing, or refactoring .NET applications that benefit from structured domain modeling and layered architecture.
Core Principles
Domain-Driven Design (DDD)
DDD aligns software design with business reality. The goal is to make the codebase a direct expression of business concepts, so domain experts and developers share a common vocabulary and the system remains adaptable as the business evolves.
- Ubiquitous Language: Use consistent business terminology throughout code, documentation, and conversation. When the business calls something an "Order," the code should use
Order -- not PurchaseRecord or TransactionItem. This reduces translation errors and keeps the model honest.
- Bounded Contexts: Define clear service boundaries where a particular domain model applies. Each bounded context has its own ubiquitous language and data ownership. Communicate across boundaries through well-defined contracts (domain events, APIs), not shared databases.
- Aggregates: Design consistency boundaries that protect business invariants. Each aggregate has a root entity that controls access to its children and enforces rules within a single transaction. Keep aggregates small -- large aggregates cause contention and performance issues.
- Domain Events: Capture business-significant occurrences as first-class objects (e.g.,
OrderPlaced, PaymentReceived). Domain events decouple bounded contexts, enable audit trails, and support eventual consistency across service boundaries.
- Rich Domain Models: Place business logic in the domain layer, not in application services or controllers. Entities and value objects should enforce their own invariants, making invalid states unrepresentable.
For detailed patterns including aggregate design rules, value object implementation, and domain event handling, see references/domain-layer.md.
SOLID Principles
These principles guide class and module design toward code that is easier to understand, extend, and test.
- Single Responsibility Principle (SRP): A class should have one reason to change. If a class handles both order validation and email notification, split it -- changes to email templates should not risk breaking validation logic.
- Open/Closed Principle (OCP): Design classes to be extended without modification. Use abstractions (interfaces, base classes) and patterns like Strategy or Decorator so new behavior can be added by writing new code rather than editing existing code.
- Liskov Substitution Principle (LSP): Subtypes must be substitutable for their base types without altering program correctness. If
PremiumCustomer extends Customer, any code that works with Customer should work identically with PremiumCustomer.
- Interface Segregation Principle (ISP): Prefer small, focused interfaces over large ones. A class should not be forced to implement methods it does not use. Split
IOrderService into IOrderReader and IOrderWriter if some consumers only need read access.
- Dependency Inversion Principle (DIP): High-level modules should depend on abstractions, not concrete implementations. The domain layer defines interfaces; the infrastructure layer provides implementations. This keeps the domain portable and testable.
.NET Good Practices
- Async programming: Use
async/await throughout the stack. Avoid blocking calls (.Result, .Wait()) that risk deadlocks and thread pool starvation.
- Dependency injection: Register services in the DI container with appropriate lifetimes (
Scoped for per-request, Singleton for stateless services, Transient for lightweight disposable objects).
- LINQ: Prefer LINQ for collection operations -- it is declarative and composable. Avoid materializing large collections unnecessarily; use
IQueryable<T> to push filtering to the database.
- Exception handling: Throw domain-specific exceptions for business rule violations. Use middleware or filters for cross-cutting exception handling. Do not use exceptions for control flow.
- Modern C# features: Leverage records for immutable DTOs, primary constructors, pattern matching, nullable reference types, and
required properties to reduce boilerplate and catch errors at compile time.
Layered Architecture
Organize code into layers with clear dependency direction: outer layers depend on inner layers, never the reverse.
Common Project Structure
A typical service repository contains:
src/
MyService.Api/ # Minimal API host, endpoints, middleware
MyService.Domain/ # Business logic, aggregates, command handlers, processors
MyService.Events/ # Domain event definitions (shared as NuGet package)
MyService.Api.Contracts/ # API request/response DTOs (shared as NuGet package)
MyService.Api.Client/ # Generated typed HTTP client for consumers
MyService.Functions/ # Azure Functions for event-driven processing (optional)
MyService.AppHost/ # Aspire orchestration (optional)
test/
MyService.Domain.Tests/
MyService.Api.Tests/
This structure separates publishable contracts (Events, Api.Contracts, Api.Client) from internal implementation (Domain, Api). Other services depend on the contracts packages, not the implementation.
Domain Layer (innermost)
The heart of the application. Contains business logic with zero dependencies on infrastructure or frameworks.
- Aggregates and Entities: enforce business invariants
- Value Objects: immutable, equality by value (use records:
Money, EmailAddress)
- Domain Services and Processors: logic that processes events or commands
- Domain Events: business-significant occurrences defined as records
- Command Handlers: CQRS command handling within the domain layer
For detailed domain layer patterns, see references/domain-layer.md.
Application / API Layer
Thin orchestration layer that wires up the domain and exposes it via HTTP:
- Minimal APIs: prefer
app.MapEndpoints() over MVC controllers
- DTOs / Contracts: separate projects for API contracts, shared as NuGet packages
- Input Validation: validate inputs at the boundary using FluentValidation
- DI Registration: fluent extension method chains on
IServiceCollection
For detailed application and infrastructure patterns, see references/application-infrastructure.md.
Infrastructure Concerns
Concrete implementations injected via DI:
- Data Access:
ICosmosReader<T> / ICosmosWriter<T> for Cosmos DB (via Atc.Cosmos), or EF Core repositories for relational databases
- Event Store: Atc.Cosmos.EventStore for event-sourced aggregates
- Event Bus: Event Hub / Service Bus processors for cross-service communication
- External Service Adapters: typed HTTP clients behind domain-defined interfaces
For detailed infrastructure patterns, see references/application-infrastructure.md.
Event Sourcing and CQRS
Many .NET services use event sourcing with CQRS as their core architectural pattern:
- Events as source of truth: all state changes captured as immutable domain events stored in Cosmos DB
- Projections / Views: read models built by projecting event streams (e.g.,
DeviceView from device events)
- Command handlers: process commands, validate business rules, and emit events
- Command processor factory:
ICommandProcessorFactory dispatches commands to the correct handler without MediatR
- Separate Events project: domain events are published as NuGet packages so other services can consume them without coupling to the domain implementation
Testing Standards
Well-structured tests protect domain invariants and enable confident refactoring. Use the test naming convention established in the project — common patterns include Should_<Expected>_When_<Condition> or MethodName_Condition_ExpectedResult.
- Unit Tests: Isolate domain logic. Test command handlers, aggregates, processors, and value objects. Use
[AutoNSubstituteData] (from Atc.Test) with [Frozen] for automatic mock injection. Use FluentAssertions for assertions.
- Integration Tests: Verify persistence, event projection, and cross-layer interactions.
- Acceptance Tests: Validate end-to-end user scenarios through the application layer.
- Coverage Target: Aim for a minimum of 85% code coverage in the domain and application layers.
For detailed testing guidance, see references/testing.md.
Implementation Workflow
When building or refactoring a feature, follow this sequence to ensure architectural alignment.
1. Domain Analysis
- Identify domain concepts, entities, and value objects
- Define aggregate boundaries (what must be consistent within a single transaction?)
- Establish ubiquitous language with stakeholders
- Document business rules and invariants
2. Architecture Review
- Verify layer responsibilities are respected (no business logic in controllers, no infrastructure in the domain)
- Check SOLID adherence across the design
- Identify domain events needed for cross-boundary communication
- Review security boundaries and data access patterns
3. Implementation Planning
- List files to create or modify, organized by layer
- Define test cases for each aggregate and application service
- Plan error handling strategy (domain exceptions, validation errors, infrastructure failures)
- Consider performance implications (query patterns, caching, lazy loading)
4. Execution
- Start with the domain model: entities, value objects, aggregate roots
- Build aggregates with invariant enforcement
- Implement application services that orchestrate domain operations
- Write tests alongside implementation (not after)
- Wire up domain events and handlers
5. Post-Implementation Review
- Verify all business invariants are enforced by aggregates
- Confirm no domain logic has leaked into application or infrastructure layers
- Check that all public APIs use DTOs rather than domain entities
- Validate test coverage meets the 85% target for domain and application layers
- Review for SOLID violations introduced during implementation
Financial Domain Considerations
Financial systems have stricter requirements around precision, auditability, and compliance. When working in a financial context, apply these additional practices.
For detailed financial domain guidance, see references/financial-domain.md.
Key points:
- Use
decimal for all monetary values -- never float or double, which introduce rounding errors
- Model money as a currency-aware value object (amount + currency code) to prevent accidental arithmetic across currencies
- Use the Saga pattern for distributed transactions that span multiple aggregates or services
- Maintain immutable audit trails via domain events -- every state change should be traceable
- Design for regulatory compliance (PCI-DSS for payment data, SOX for financial reporting) from the start, not as an afterthought
1---2name: dotnet-architecture3description: Best practices for .NET application architecture based on Domain-Driven Design (DDD), SOLID principles, and layered architecture. Covers aggregate design, value objects, domain events, bounded contexts, domain services, application services, repository patterns, specification patterns, and infrastructure concerns. Includes testing standards (unit, integration, acceptance) with minimum coverage targets, step-by-step implementation workflows, and financial domain considerations (monetary value handling, saga patterns, audit trails, regulatory compliance). Apply this skill whenever designing, building, reviewing, or refactoring .NET application architecture -- including when the user mentions domain models, aggregates, bounded contexts, domain events, layered architecture, SOLID violations, repository patterns, or DDD concepts, even if they do not explicitly say "architecture."4---56# .NET Architecture Best Practices78Apply these practices when designing, building, reviewing, or refactoring .NET applications that benefit from structured domain modeling and layered architecture.910## Core Principles1112### Domain-Driven Design (DDD)1314DDD aligns software design with business reality. The goal is to make the codebase a direct expression of business concepts, so domain experts and developers share a common vocabulary and the system remains adaptable as the business evolves.1516- **Ubiquitous Language**: Use consistent business terminology throughout code, documentation, and conversation. When the business calls something an "Order," the code should use `Order` -- not `PurchaseRecord` or `TransactionItem`. This reduces translation errors and keeps the model honest.17- **Bounded Contexts**: Define clear service boundaries where a particular domain model applies. Each bounded context has its own ubiquitous language and data ownership. Communicate across boundaries through well-defined contracts (domain events, APIs), not shared databases.18- **Aggregates**: Design consistency boundaries that protect business invariants. Each aggregate has a root entity that controls access to its children and enforces rules within a single transaction. Keep aggregates small -- large aggregates cause contention and performance issues.19- **Domain Events**: Capture business-significant occurrences as first-class objects (e.g., `OrderPlaced`, `PaymentReceived`). Domain events decouple bounded contexts, enable audit trails, and support eventual consistency across service boundaries.20- **Rich Domain Models**: Place business logic in the domain layer, not in application services or controllers. Entities and value objects should enforce their own invariants, making invalid states unrepresentable.2122For detailed patterns including aggregate design rules, value object implementation, and domain event handling, see [references/domain-layer.md](references/domain-layer.md).2324### SOLID Principles2526These principles guide class and module design toward code that is easier to understand, extend, and test.2728- **Single Responsibility Principle (SRP)**: A class should have one reason to change. If a class handles both order validation and email notification, split it -- changes to email templates should not risk breaking validation logic.29- **Open/Closed Principle (OCP)**: Design classes to be extended without modification. Use abstractions (interfaces, base classes) and patterns like Strategy or Decorator so new behavior can be added by writing new code rather than editing existing code.30- **Liskov Substitution Principle (LSP)**: Subtypes must be substitutable for their base types without altering program correctness. If `PremiumCustomer` extends `Customer`, any code that works with `Customer` should work identically with `PremiumCustomer`.31- **Interface Segregation Principle (ISP)**: Prefer small, focused interfaces over large ones. A class should not be forced to implement methods it does not use. Split `IOrderService` into `IOrderReader` and `IOrderWriter` if some consumers only need read access.32- **Dependency Inversion Principle (DIP)**: High-level modules should depend on abstractions, not concrete implementations. The domain layer defines interfaces; the infrastructure layer provides implementations. This keeps the domain portable and testable.3334### .NET Good Practices3536- **Async programming**: Use `async`/`await` throughout the stack. Avoid blocking calls (`.Result`, `.Wait()`) that risk deadlocks and thread pool starvation.37- **Dependency injection**: Register services in the DI container with appropriate lifetimes (`Scoped` for per-request, `Singleton` for stateless services, `Transient` for lightweight disposable objects).38- **LINQ**: Prefer LINQ for collection operations -- it is declarative and composable. Avoid materializing large collections unnecessarily; use `IQueryable<T>` to push filtering to the database.39- **Exception handling**: Throw domain-specific exceptions for business rule violations. Use middleware or filters for cross-cutting exception handling. Do not use exceptions for control flow.40- **Modern C# features**: Leverage records for immutable DTOs, primary constructors, pattern matching, nullable reference types, and `required` properties to reduce boilerplate and catch errors at compile time.4142## Layered Architecture4344Organize code into layers with clear dependency direction: outer layers depend on inner layers, never the reverse.4546### Common Project Structure4748A typical service repository contains:4950```51src/52 MyService.Api/ # Minimal API host, endpoints, middleware53 MyService.Domain/ # Business logic, aggregates, command handlers, processors54 MyService.Events/ # Domain event definitions (shared as NuGet package)55 MyService.Api.Contracts/ # API request/response DTOs (shared as NuGet package)56 MyService.Api.Client/ # Generated typed HTTP client for consumers57 MyService.Functions/ # Azure Functions for event-driven processing (optional)58 MyService.AppHost/ # Aspire orchestration (optional)59test/60 MyService.Domain.Tests/61 MyService.Api.Tests/62```6364This structure separates publishable contracts (Events, Api.Contracts, Api.Client) from internal implementation (Domain, Api). Other services depend on the contracts packages, not the implementation.6566### Domain Layer (innermost)6768The heart of the application. Contains business logic with zero dependencies on infrastructure or frameworks.6970- **Aggregates and Entities**: enforce business invariants71- **Value Objects**: immutable, equality by value (use records: `Money`, `EmailAddress`)72- **Domain Services and Processors**: logic that processes events or commands73- **Domain Events**: business-significant occurrences defined as records74- **Command Handlers**: CQRS command handling within the domain layer7576For detailed domain layer patterns, see [references/domain-layer.md](references/domain-layer.md).7778### Application / API Layer7980Thin orchestration layer that wires up the domain and exposes it via HTTP:8182- **Minimal APIs**: prefer `app.MapEndpoints()` over MVC controllers83- **DTOs / Contracts**: separate projects for API contracts, shared as NuGet packages84- **Input Validation**: validate inputs at the boundary using FluentValidation85- **DI Registration**: fluent extension method chains on `IServiceCollection`8687For detailed application and infrastructure patterns, see [references/application-infrastructure.md](references/application-infrastructure.md).8889### Infrastructure Concerns9091Concrete implementations injected via DI:9293- **Data Access**: `ICosmosReader<T>` / `ICosmosWriter<T>` for Cosmos DB (via Atc.Cosmos), or EF Core repositories for relational databases94- **Event Store**: Atc.Cosmos.EventStore for event-sourced aggregates95- **Event Bus**: Event Hub / Service Bus processors for cross-service communication96- **External Service Adapters**: typed HTTP clients behind domain-defined interfaces9798For detailed infrastructure patterns, see [references/application-infrastructure.md](references/application-infrastructure.md).99100## Event Sourcing and CQRS101102Many .NET services use event sourcing with CQRS as their core architectural pattern:103104- **Events as source of truth**: all state changes captured as immutable domain events stored in Cosmos DB105- **Projections / Views**: read models built by projecting event streams (e.g., `DeviceView` from device events)106- **Command handlers**: process commands, validate business rules, and emit events107- **Command processor factory**: `ICommandProcessorFactory` dispatches commands to the correct handler without MediatR108- **Separate Events project**: domain events are published as NuGet packages so other services can consume them without coupling to the domain implementation109110## Testing Standards111112Well-structured tests protect domain invariants and enable confident refactoring. Use the test naming convention established in the project — common patterns include `Should_<Expected>_When_<Condition>` or `MethodName_Condition_ExpectedResult`.113114- **Unit Tests**: Isolate domain logic. Test command handlers, aggregates, processors, and value objects. Use `[AutoNSubstituteData]` (from Atc.Test) with `[Frozen]` for automatic mock injection. Use FluentAssertions for assertions.115- **Integration Tests**: Verify persistence, event projection, and cross-layer interactions.116- **Acceptance Tests**: Validate end-to-end user scenarios through the application layer.117- **Coverage Target**: Aim for a minimum of 85% code coverage in the domain and application layers.118119For detailed testing guidance, see [references/testing.md](references/testing.md).120121## Implementation Workflow122123When building or refactoring a feature, follow this sequence to ensure architectural alignment.124125### 1. Domain Analysis126- Identify domain concepts, entities, and value objects127- Define aggregate boundaries (what must be consistent within a single transaction?)128- Establish ubiquitous language with stakeholders129- Document business rules and invariants130131### 2. Architecture Review132- Verify layer responsibilities are respected (no business logic in controllers, no infrastructure in the domain)133- Check SOLID adherence across the design134- Identify domain events needed for cross-boundary communication135- Review security boundaries and data access patterns136137### 3. Implementation Planning138- List files to create or modify, organized by layer139- Define test cases for each aggregate and application service140- Plan error handling strategy (domain exceptions, validation errors, infrastructure failures)141- Consider performance implications (query patterns, caching, lazy loading)142143### 4. Execution144- Start with the domain model: entities, value objects, aggregate roots145- Build aggregates with invariant enforcement146- Implement application services that orchestrate domain operations147- Write tests alongside implementation (not after)148- Wire up domain events and handlers149150### 5. Post-Implementation Review151- Verify all business invariants are enforced by aggregates152- Confirm no domain logic has leaked into application or infrastructure layers153- Check that all public APIs use DTOs rather than domain entities154- Validate test coverage meets the 85% target for domain and application layers155- Review for SOLID violations introduced during implementation156157## Financial Domain Considerations158159Financial systems have stricter requirements around precision, auditability, and compliance. When working in a financial context, apply these additional practices.160161For detailed financial domain guidance, see [references/financial-domain.md](references/financial-domain.md).162163Key points:164- Use `decimal` for all monetary values -- never `float` or `double`, which introduce rounding errors165- Model money as a currency-aware value object (amount + currency code) to prevent accidental arithmetic across currencies166- Use the Saga pattern for distributed transactions that span multiple aggregates or services167- Maintain immutable audit trails via domain events -- every state change should be traceable168- Design for regulatory compliance (PCI-DSS for payment data, SOX for financial reporting) from the start, not as an afterthought