- A maintainable and testable .NET 8 monolith built with Domain-Driven Design (DDD), CQRS, and Clean Architecture. It supports Minimal APIs, MediatR, Testcontainers, and is deployable via Docker Compose and Azure Container Apps.
Clean Architecture is a layered software design model that emphasizes separation of concerns and inward-facing dependencies. It helps build systems that are maintainable, testable, and independent of frameworks, databases, or UI.
-
Domain
-
Domain models and logic that define the core of the system
-
Independent of any external technology
-
Examples: Entities, Value Objects, Domain services, Domain events, Enums, Repository interfaces
-
-
Application
-
Contains the application use cases
-
Contains application-specific business rules
-
Orchestrates the domain entities to perform business operations
-
It should be independent of external concerns (but it doesn't have to be)
-
Examples: Application services, Commands, Queries, External service interfaces, Exceptions
-
-
Presentation
-
Represents the entry point to the system
-
Accepts data from the outside and passes it to the use cases
-
Acts as the composition root for dependency injection
-
Examples: ASP.NET Core API, MVC, Razor Pages, gRPC
-
-
Infrastructure
-
Contains anything related to external concerns
-
Implements interfaces defined in the layers below
-
Examples: PostgreSQL, Keycloak, AWS S3, RabbitMQ, Kafka, SendGrid
-
CQRS (Command Query Responsibility Segregation) separates read and write operations to improve scalability, maintainability, and performance. Below are two common design styles:
-
Commands and queries use the same database
-
Read/write logic separated, but infrastructure shared
-
Easier to implement, strong consistency
-
Ideal for monoliths, internal business apps
-
Write model handles commands and domain logic
-
Read model optimized for queries and DTOs
-
Data sync via events or messaging
-
Enables scalable reads, eventual consistency
-
Ideal for distributed systems, microservices
Domain-Driven Design helps you build software that reflects business logic first, not technical details. It reduces complexity, increases clarity, and scales better.
-
Business rules at the center
-
Code that mirrors the domain language
-
Domain logic isolated from infrastructure
-
A shared language between developers and domain experts.
-
All class names, methods, events, and use cases use business terminology consistently.
-
A clear boundary where a specific domain model applies.
-
Examples in an eCommerce system:
Ordering,Inventory,Payments -
Bounded Contexts communicate via: Domain Events, Integration Events, Message Brokers (RabbitMQ, Kafka, MediatR notifications in monolith)
-
Has a unique identity
-
State changes over time
-
Example:
public class Order { public OrderId Id { get; private set; } public OrderStatus Status { get; private set; } }
-
No identity
-
Immutable
-
Compared by value
-
Example:
Address,Money. Two value objects are equal if all their values match.
-
Aggregate = a cluster of related objects with rules ensuring consistency.
-
Aggregate Root = the only entry point to modify the aggregate.
-
Rules:
-
Do not expose internal entities
-
All changes go through the root
-
A transaction affects only one Aggregate
-
-
Overview
-
Make side effects explicit.
-
Keep domain logic clean and decoupled.
-
Can run sync or async.
-
Trade-offs: eventual consistency, handlers may fail.
-
-
Publishing Domain Events
-
Publish BEFORE SaveChanges ❌ (Not Recommended)
-
Events run inside EF transaction.
-
External side effects (email, HTTP, queue) cannot be rolled back.
-
Handler failure → DB rollback but side effects remain → inconsistency.
-
Only safe if all handlers touch the same DB only.
await PublishDomainEventsAsync(); return await base.SaveChangesAsync(); -
-
Publish AFTER SaveChanges ✅ (Recommended)
-
The database transaction commits first → the state is guaranteed to be durable.
-
Domain event handlers run outside the transaction → failures don’t affect the database and can be retried safely.
-
Supports the Outbox Pattern, which:
-
Prevents lost events.
-
Allows reliable retries when publishing.
-
Keeps database state and published messages consistent.
-
Works well in microservices and distributed systems.
-
Overall, this approach is more reliable and production-ready.
-
var result = await base.SaveChangesAsync(); await PublishDomainEventsAsync(); return result; -
-
-
Compare with Integration Events
-
Domain Events:
-
Published and consumed within a single domain
-
Sent using an in-memory message bus
-
Can be processed synchronously or asynchronously
-
-
Integration Events:
-
Consumed by other subsystems (microservices, Bounded Contexts)
-
Sent with a message broker over a queue
-
Processed completely asynchronously
-
-
-
Used when logic does not naturally belong to any entity/value object.
-
Examples:
Calculating shipping cost,Currency conversion,Cross-aggregate business rules
-
Orchestrates use cases:
-
No business logic
-
Calls domain + repositories
-
Handles commands/queries
-
Example:
Command Handlers,Mediator Handlers,Application Services
-
Manages only Aggregate Roots:
-
Load/save aggregates
-
Hide persistence (EF Core, MongoDB, SQL, Redis…)
public interface IOrderRepository { Task<Order?> GetAsync(OrderId id); Task AddAsync(Order order); }
-
Encapsulates business filtering rules.
-
Used for queries or domain-level validation.
-
Application: Defines use cases, handles domain events, and manages business logic via commands and queries (CQRS).
-
Domain: Contains core models — entities, value objects, aggregates, domain events.
-
Infrastructure: Implements technical concerns — database, caching, outbox, external services.
-
Presentation: Exposes APIs, controllers, endpoints to interact with the system.
-
Tests: Validates behavior and architecture across layers.
- Minimal API with MediatR and clean separation of concerns
- CQRS with command and query handlers
- DDD with rich domain models and events
- Testcontainers for integration testing
- Azure Key Vault integration for secret management
- Docker Compose for local orchestration
- Azure Container Apps for cloud deployment
git clone https://github.com/sonnh02-dev/DddCqrs.git
cd DddCqrs
-
.NET SDK 8+
-
Docker & Docker Compose
-
Azure CLI (for deployment and Key Vault)
dotnet restore
dotnet build
docker compose --profile infra --profile api up
-
Create a resource group (e.g.
DddCqrs) and provision the following services:Resource Name Type Purpose ddd-cqrs-acaAzure Container App Hosts the application dddcqrsacrAzure Container Registry Stores Docker images ddd-cqrs-dbAzure SQL Application database ddd-cqrs-kvAzure Key Vault Stores secrets securely
-
Enable Managed Identity for your Container App:
Azure Portal→ddd-cqrs-aca→Identity→System assigned→Enable -
Assign "Key Vault Secrets User" role to the Container App:
Azure Portal→ddd-cqrs-kv→Access control (IAM)→Add role assignment
-
Set secret values in
ddd-cqrs-kv -
Secrets must follow this naming pattern for automatic configuration mapping:
AwsS3--AccessKey AwsS3--SecretKey AzureBlob--ConnectionString ....
4.5.5.1. Deploy via Visual Studio
-
Deploy to Azure Container App
4.5.5.2. Or use GitHub Actions:
-
GitHub Actions workflow includes:
-
Restore, build, and test solution
-
Build Docker image and push to Azure Container Registry
-
Deploy image to Azure Container App
-
-
Environment variable:
- DOTNET_VERSION: "8.0.x"
- IMAGE_NAME: sonnh02dev/dddcqrs.presentation
- CONTAINER_APP_NAME: ddd-cqrs-aca
- RESOURCE_GROUP: DddCqrs
- AZURE_CONTAINER_REGISTRY: dddcqrsacr
-
Secrets required:
-
AZURE_REGISTRY_USERNAME
-
AZURE_REGISTRY_PASSWORD
-
AZURE_CREDENTIALS (Service Principal JSON)
-














