Skip to main content

Introduction

The Hybrid DDD Architecture is organized into three distinct layers, each with specific responsibilities and dependencies. This layered architecture ensures separation of concerns, maintainability, and testability.

Layer Overview

Domain

Business entities, validators, and domain logic

Application

Use cases, commands, queries, and orchestration

Infrastructure

Persistence, messaging, and external integrations

Dependency Flow

Dependencies flow inward toward the domain core, following the Dependency Inversion Principle:
Infrastructure Layer
       ↓ (depends on)
Application Layer
       ↓ (depends on)
  Domain Layer
       ↓ (depends on)
   Core Modules
The Domain layer has no dependencies on other application layers, making it highly testable and portable.

Domain Layer

The Domain Layer contains the core business logic and is the heart of the application. It is framework-agnostic and has no dependencies on infrastructure concerns.

Purpose

  • Encapsulate business rules and invariants
  • Define domain entities and value objects
  • Validate domain state
  • Express the ubiquitous language of the business

Components

Domain Entities

Rich domain objects that encapsulate business logic and maintain invariants.
public class DummyEntity : DomainEntity<string, DummyEntityValidator>
{
    /// <summary>
    /// Properties have private setters to restrict modifications
    /// from the Application layer
    /// </summary>
    public string DummyPropertyOne { get; private set; }
    public DummyValues DummyPropertyTwo { get; private set; }

    public DummyEntity(string dummyPropertyOne, DummyValues dummyPropertyTwo)
    {
        Id = Guid.NewGuid().ToString();
        SetdummyPropertyOne(dummyPropertyOne);
        DummyPropertyTwo = dummyPropertyTwo;
    }

    public void SetdummyPropertyOne(string value)
    {
        DummyPropertyOne = value ?? throw new ArgumentNullException(nameof(value));
    }

    public void SetdummyPropertyTwo(DummyValues value)
    {
        DummyPropertyTwo = value;
    }
}
Source: Domain/Entities/DummyEntity.cs:12-47
Key Design Principle: Domain entity properties have private setters. This restricts modifications and ensures that changes only occur through domain methods that enforce business rules.

Validators

FluentValidation-based validators that define business rules.
public class DummyEntityValidator : EntityValidator<DummyEntity>
{
    public DummyEntityValidator()
    {
        // Business rules are defined here
        RuleFor(x => x.DummyPropertyOne)
            .NotNull()
            .NotEmpty()
            .WithMessage(DomainConstants.NOTNULL_OR_EMPTY);
    }
}
Source: Domain/Validators/DummyEntityValidator.cs:13-19

Domain Constants and Enums

// Domain constants
public static class DomainConstants
{
    public const string NOTNULL_OR_EMPTY = "Value cannot be null or empty";
}

// Domain enums
public static class Enums
{
    public enum DummyValues
    {
        Value1,
        Value2,
        Value3
    }
}

Directory Structure

Domain/
├── Constants/
│   └── Constants.cs
├── Entities/
│   └── DummyEntity.cs
├── Enums/
│   └── Enums.cs
├── Validators/
│   └── DummyEntityValidator.cs
└── Others/
    └── Utils/
        └── EnumUtils.cs

Key Characteristics

The Domain layer only depends on Core.Domain and has no references to databases, frameworks, or infrastructure.
Entities contain behavior, not just data. They encapsulate business logic and enforce invariants.
Entities inherit from DomainEntity<TKey, TValidator> and validate themselves using FluentValidation.
Private setters and methods control state changes, preventing invalid states.

Application Layer

The Application Layer orchestrates domain logic to implement use cases. It defines interfaces for infrastructure concerns and coordinates the flow of data.

Purpose

  • Implement use cases and business workflows
  • Define commands and queries (CQRS)
  • Declare repository and adapter interfaces
  • Handle domain events
  • Map between domain entities and DTOs

Components

Use Cases (Commands)

Commands represent write operations that change system state.
public class CreateDummyEntityCommand : IRequestCommand<string>
{
    [Required]
    public string dummyPropertyOne { get; set; }
    public DummyValues dummyPropertyTwo { get; set; }

    public CreateDummyEntityCommand()
    {
    }
}
Source: Application/UseCases/DummyEntity/Commands/CreateDummyEntity/CreateDummyEntityCommand.cs:13-22

Command Handlers

Handlers process commands and orchestrate domain logic.
internal sealed class CreateDummyEntityHandler : IRequestCommandHandler<CreateDummyEntityCommand, string>
{
    private readonly ICommandQueryBus _domainBus;
    private readonly IDummyEntityRepository _context;
    private readonly IDummyEntityApplicationService _dummyEntityApplicationService;

    public CreateDummyEntityHandler(
        ICommandQueryBus domainBus, 
        IDummyEntityRepository dummyEntityRepository, 
        IDummyEntityApplicationService dummyEntityApplicationService)
    {
        _domainBus = domainBus ?? throw new ArgumentNullException(nameof(domainBus));
        _context = dummyEntityRepository ?? throw new ArgumentNullException(nameof(dummyEntityRepository));
        _dummyEntityApplicationService = dummyEntityApplicationService ?? throw new ArgumentNullException(nameof(dummyEntityApplicationService));
    }

    public async Task<string> Handle(CreateDummyEntityCommand request, CancellationToken cancellationToken)
    {
        // Create domain entity
        Domain.Entities.DummyEntity entity = new(request.dummyPropertyOne, request.dummyPropertyTwo);

        // Validate domain entity
        if (!entity.IsValid) 
            throw new InvalidEntityDataException(entity.GetErrors());

        // Check business rules
        if (_dummyEntityApplicationService.DummyEntityExist(entity.Id)) 
            throw new EntityDoesExistException();

        try
        {
            // Persist entity
            object createdId = await _context.AddAsync(entity);

            // Publish domain event
            await _domainBus.Publish(entity.To<DummyEntityCreated>(), cancellationToken);

            return createdId.ToString();
        }
        catch (Exception ex)
        {
            throw new BussinessException(ApplicationConstants.PROCESS_EXECUTION_EXCEPTION, ex.InnerException);
        }
    }
}
Source: Application/UseCases/DummyEntity/Commands/CreateDummyEntity/CreateDummyEntityHandler.cs:17-43

Use Cases (Queries)

Queries represent read operations that retrieve data.
public class GetAllDummyEntitiesQuery : QueryRequest<QueryResult<DummyEntityDto>>
{
}
Source: Application/UseCases/DummyEntity/Queries/GetAllDummyEntities/GetAllDummyEntitiesQuery.cs:6-8

Query Handlers

Handlers process queries and return data.
internal class GetAllDummyEntitiesHandler : IRequestQueryHandler<GetAllDummyEntitiesQuery, QueryResult<DummyEntityDto>>
{
    private readonly IDummyEntityRepository _context;

    public GetAllDummyEntitiesHandler(IDummyEntityRepository context)
    {
        _context = context ?? throw new ArgumentNullException(nameof(context));
    }

    public async Task<QueryResult<DummyEntityDto>> Handle(GetAllDummyEntitiesQuery request, CancellationToken cancellationToken)
    {
        IList<Domain.Entities.DummyEntity> entities = await _context.FindAllAsync();

        return new QueryResult<DummyEntityDto>(
            entities.To<DummyEntityDto>(), 
            entities.Count, 
            request.PageIndex, 
            request.PageSize);
    }
}
Source: Application/UseCases/DummyEntity/Queries/GetAllDummyEntities/GetAllDummyEntitiesHandler.cs:7-17

Domain Events

Events that represent something that happened in the domain.
internal sealed class DummyEntityCreated : DomainEvent
{
    public string Id { get; set; }
    public string DummyPropertyOne { get; set; }
    public DummyValues DummyPropertyTwo { get; set; }
}
Source: Application/DomainEvents/DummyEntityCreated.cs:10-15

Repository Interfaces

Define contracts for data access.
public interface IDummyEntityRepository : IRepository<DummyEntity>
{
    // Custom repository methods can be defined here
}
Source: Application/Repositories/IDummyEntityRepository.cs:11-14

Data Transfer Objects (DTOs)

Simple objects for transferring data across boundaries.
public class DummyEntityDto
{
    public string Id { get; set; }
    public string DummyPropertyOne { get; set; }
    public DummyValues DummyPropertyTwo { get; set; }
}

Mapping Profiles

Define AutoMapper mappings between objects.
public class Mapping : Profile
{
    public Mapping()
    {
        CreateMap<DummyEntity, DummyEntityCreated>().ReverseMap();
        CreateMap<DummyEntity, DummyEntityUpdated>().ReverseMap();
        CreateMap<DummyEntity, DummyEntityDto>().ReverseMap();
    }
}
Source: Application/Mappings/Mapping.cs:11-18

Directory Structure

Application/
├── ApplicationServices/
│   ├── DummyEntityApplicationService.cs
│   └── IDummyEntityApplicationService.cs
├── Constants/
│   └── Constants.cs
├── DataTransferObjects/
│   └── DummyEntityDto.cs
├── DomainEvents/
│   ├── DummyEntityCreated.cs
│   ├── DummyEntityDeleted.cs
│   └── DummyEntityUpdated.cs
├── Exceptions/
│   └── Exceptions.cs
├── Integrations/
│   ├── Events/
│   └── Handlers/
├── Mappings/
│   └── Mapping.cs
├── Repositories/
│   └── IDummyEntityRepository.cs
├── Registrations/
│   └── ApplicationServicesRegistration.cs
└── UseCases/
    └── DummyEntity/
        ├── Commands/
        │   ├── CreateDummyEntity/
        │   ├── DeleteDummyEntity/
        │   └── UpdateDummyEntity/
        ├── Queries/
        │   ├── GetAllDummyEntities/
        │   └── GetDummyEntityBy/
        └── Notifications/

Key Characteristics

The Application layer coordinates domain objects to fulfill use cases but contains no business logic itself.
Defines interfaces for infrastructure dependencies (repositories, external APIs) but doesn’t implement them.
Separates commands (writes) from queries (reads) for better scalability and maintainability.
Publishes domain events that can trigger side effects and integration events.

Infrastructure Layer

The Infrastructure Layer provides concrete implementations of interfaces defined in the Application layer. It handles technical concerns like persistence, messaging, and external integrations.

Purpose

  • Implement repository interfaces
  • Provide database contexts and configurations
  • Implement event bus with messaging infrastructure
  • Integrate with external APIs and services
  • Handle cross-cutting concerns

Components

Repository Implementations

MongoDB Implementation:
internal sealed class DummyEntityRepository : BaseRepository<DummyEntity>, IDummyEntityRepository
{
    public DummyEntityRepository(StoreDbContext context) : base(context)
    {
    }
}
Source: Infrastructure/Repositories/Mongo/DummyEntityRepository.cs:13-17 The MongoDB repository inherits from BaseRepository<TEntity> which is provided by the Core.Infrastructure.Repositories.MongoDb module. SQL Server Implementation:
internal sealed class DummyEntityRepository : BaseRepository<DummyEntity>, IDummyEntityRepository
{
    public DummyEntityRepository(StoreDbContext context) : base(context)
    {
    }
}
The SQL Server repository uses Entity Framework Core for data access.

Database Contexts

MongoDB Context:
public class StoreDbContext : DbContext
{
    public StoreDbContext(IMongoDatabase database) : base(database)
    {
    }
}
SQL Server Context:
public class StoreDbContext : DbContext
{
    public DbSet<DummyEntity> DummyEntities { get; set; }

    public StoreDbContext(DbContextOptions<StoreDbContext> options) : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Entity configurations
    }
}

Entity Mappings (MongoDB)

public class DummyEntityMap : IEntityTypeConfiguration<DummyEntity>
{
    public void Configure(EntityTypeBuilder<DummyEntity> builder)
    {
        builder.ToCollection("DummyEntities");
        
        builder.Property(e => e.Id)
               .IsRequired();
               
        builder.Property(e => e.DummyPropertyOne)
               .IsRequired();
    }
}

Database Factory

Factory pattern for creating database instances.
public class DatabaseFactory
{
    public static IMongoDatabase CreateMongoDatabase(string connectionString, string databaseName)
    {
        var client = new MongoClient(connectionString);
        return client.GetDatabase(databaseName);
    }
}

Service Registration

Dependency injection configuration.
public static class InfrastructureServicesRegistration
{
    public static IServiceCollection AddInfrastructureServices(this IServiceCollection services, IConfiguration configuration)
    {
        // Register repositories
        services.AddScoped<IDummyEntityRepository, DummyEntityRepository>();
        
        // Register event bus
        services.AddSingleton<IEventBus, RabbitMqEventBus>();
        
        // Register database context
        // ... configuration
        
        return services;
    }
}

Directory Structure

Infrastructure/
├── Constants/
│   └── Constants.cs
├── Factories/
│   └── DatabaseFactory.cs
├── Registrations/
│   └── InfrastructureServicesRegistration.cs
└── Repositories/
    ├── Mongo/
    │   ├── DummyEntityRepository.cs
    │   ├── Maps/
    │   │   └── DummyEntityMap.cs
    │   └── StoreDbContext.cs
    └── Sql/
        ├── DummyEntityRepository.cs
        └── StoreDbContext.cs

Key Characteristics

Contains all framework-specific code and external dependencies (EF Core, MongoDB drivers, RabbitMQ clients).
Multiple implementations of the same interface (MongoDB vs SQL Server) can coexist.
Uses service registration to wire up implementations with interfaces.
Handles connection strings, API keys, and other environment-specific settings.

Layer Interaction Example

Here’s how the layers work together when creating a new entity:

Step-by-Step Flow

  1. API Layer receives HTTP request and creates a command
  2. Application Layer handler receives command
  3. Domain Layer creates and validates entity
  4. Application Layer checks business rules via application service
  5. Infrastructure Layer persists entity to database
  6. Application Layer publishes domain event
  7. Event Handlers (Application Layer) respond to event
  8. API Layer returns response to client
Each layer has a single responsibility and communicates through well-defined interfaces.

Benefits of Layered Architecture

Separation of Concerns

Each layer has distinct responsibilities, making the codebase easier to understand and maintain.

Testability

Business logic in the Domain layer can be tested without databases or external dependencies.

Flexibility

Infrastructure implementations can be swapped without affecting business logic.

Maintainability

Changes are isolated to specific layers, reducing the risk of unintended side effects.

Next Steps

CQRS Pattern

Learn how commands and queries work together

Domain Entities

Create your first domain entity

Commands & Handlers

Implement use cases with commands

Repositories

Set up data persistence

Build docs developers (and LLMs) love