Skip to content

A maintainable and testable .NET monolith using Domain-Driven Design, Command Query Separation, and Clean Architecture. Supports Minimal APIs, Docker, and Testcontainers for integration testing.

Notifications You must be signed in to change notification settings

sonnh02-dev/DddCqrs

Repository files navigation

DddCqrs

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

Table of Contents


1. Project Architecture

1.1 Clean Architecture

alt text

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.

Layered Structure

  • 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

1.2 CQRS Pattern

CQRS (Command Query Responsibility Segregation) separates read and write operations to improve scalability, maintainability, and performance. Below are two common design styles:

1.2.1 Logical Separation (Shared Store)

Logical Separation

  • 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

1.2.2 Strict Separation (Dual Stores)

alt text

  • 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

1.3 Domain-Driven Design (DDD)

Domain-Driven Design helps you build software that reflects business logic first, not technical details. It reduces complexity, increases clarity, and scales better.

1.3.1 Key ideas:

  • Business rules at the center

  • Code that mirrors the domain language

  • Domain logic isolated from infrastructure

1.3.2 Ubiquitous Language

  • A shared language between developers and domain experts.

  • All class names, methods, events, and use cases use business terminology consistently.

1.3.3 Bounded Context

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

1.3.4 Entity

  • Has a unique identity

  • State changes over time

  • Example:

    public class Order
     {
        public OrderId Id { get; private set; }    
        public OrderStatus Status { get; private set; }
     }
    

1.3.5 Value Object

  • No identity

  • Immutable

  • Compared by value

  • Example: Address, Money. Two value objects are equal if all their values match.

1.3.6 Aggregate & Aggregate Root

  • 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

1.3.7 Domain Events

alt text

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

      alt text

      • Published and consumed within a single domain

      • Sent using an in-memory message bus

      • Can be processed synchronously or asynchronously

    • Integration Events:

      alt text

      • Consumed by other subsystems (microservices, Bounded Contexts)

      • Sent with a message broker over a queue

      • Processed completely asynchronously

1.3.8 Domain Services

  • Used when logic does not naturally belong to any entity/value object.

  • Examples: Calculating shipping cost, Currency conversion, Cross-aggregate business rules

1.3.9 Application Layer

  • Orchestrates use cases:

  • No business logic

  • Calls domain + repositories

  • Handles commands/queries

  • Example: Command Handlers, Mediator Handlers, Application Services

1.3.10 Repository

  • 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);
     }
    

1.3.11 Specification Pattern

  • Encapsulates business filtering rules.

  • Used for queries or domain-level validation.

2. Project Structure

alt text

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

3. Features

  • 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

4. How to run

4.1 Clone repo

git clone https://github.com/sonnh02-dev/DddCqrs.git
cd DddCqrs

4.2 Install prerequisites

  • .NET SDK 8+

  • Docker & Docker Compose

  • Azure CLI (for deployment and Key Vault)

4.3 Restore and build

dotnet restore
dotnet build

4.4 Run with Docker Compose

docker compose --profile infra --profile api up

4.5 Deploy to Azure Container Apps

4.5.1 Required Azure Resources

  • Create a resource group (e.g. DddCqrs) and provision the following services:

    Resource Name Type Purpose
    ddd-cqrs-aca Azure Container App Hosts the application
    dddcqrsacr Azure Container Registry Stores Docker images
    ddd-cqrs-db Azure SQL Application database
    ddd-cqrs-kv Azure Key Vault Stores secrets securely

    alt text

4.5.2 Configure IAM for Key Vault Access

  • Enable Managed Identity for your Container App: Azure Portalddd-cqrs-acaIdentitySystem assignedEnable

    alt text

  • Assign "Key Vault Secrets User" role to the Container App: Azure Portalddd-cqrs-kvAccess control (IAM)Add role assignment

    alt text

4.5.3 Store your secret in Key Vault

  • Set secret values in ddd-cqrs-kv

    alt text

  • Secrets must follow this naming pattern for automatic configuration mapping:

    AwsS3--AccessKey
    AwsS3--SecretKey
    AzureBlob--ConnectionString
    ....
    

4.5.4 Reference the secret in Azure Container App

  • Add secrets to the Container App

    alt text

  • Then navigate to Containers -. Environment variables -. Add

    alt text

4.5.5 Deploy

4.5.5.1. Deploy via Visual Studio

  • Use Publish → Docker Container Registry alt text

  • Push image to Azure Container Registry alt text

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

5. License

About

A maintainable and testable .NET monolith using Domain-Driven Design, Command Query Separation, and Clean Architecture. Supports Minimal APIs, Docker, and Testcontainers for integration testing.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published