Telerik blogs
ASP.NET Core

Domain-driven design helps align code structure with business logic, making it easier to build systems that solve complex problems. Explore the basic concepts of DDD and learn how to implement a microservice following this approach.

Domain-driven design (DDD) is an approach that deals with highly complex problems, which makes it a challenging concept for many developers to understand and practice. However, it is not necessary to implement all aspects of DDD at once. It is possible to start gradually, applying its basic principles and adding features as needed.

This post aims to present the basic concepts of DDD, implementing in practice a microservice designed with its main principles. To do this, we will understand the foundations of DDD and then build a microservice in ASP.NET Core that provides an endpoint to process a commission by applying business rules and inserting the record into an SQL Server database.

What Is DDD?

Domain-driven design is a software development approach that aims to provide a way to model software based on a well-structured business domain.

In DDD, the “domain” refers to the problem space that the software is trying to solve, and the focus is on deeply understanding the domain to create a model that accurately reflects the business concepts and rules relevant to solving the problem. For example, in a medical clinic, the domain includes business processes and regulations related to the operation of the clinic, such as patient management, appointment scheduling, medical record management and coordination of doctors and other healthcare professionals.

DDD emphasizes collaboration between developers and domain (business) experts to create a software model that reflects the logic and rules of the domain. To this end, DDD encourages the use of a ubiquitous language, where everyone involved in the project shares a common language that is reflected in both communication and code and is in harmony between the development team and the business team.

Furthermore, one of the main characteristics of systems that implement DDD is the decomposition of the system into bounded contexts, where each part of the system is developed and managed independently, but within a well-defined context. Identifying and categorizing bounded contexts helps to keep complexity under control and facilitates the evolution of the system.

When to Consider Using DDD?

Using DDD patterns and rules can lead to a steep learning curve, so it is important to consider that DDD approaches should only be applied if you implement complex systems with significant business rules. Simpler systems with few rules and validations, such as a CRUD service for registration, can be implemented using a simpler approach.

DDD is commonly used in scenarios that use microservices architecture design. In these cases, DDD helps to clearly define the boundaries of each service based on the delimited contexts.

Each microservice can be responsible for a specific domain context, facilitating cohesion and independence between services. For example, in an ecommerce application, there may be separate microservices for product management, payment processing and customer service, each modeled according to the business rules and concepts specific to their context. This allows different teams to work more efficiently and develop, scale and maintain services independently. Like microservices, DDD is about boundaries—it helps us identify them, categorize them and then transform them into software.

Strategic Design and Tactical Design

In DDD, the concepts of strategic design and tactical design play complementary roles in modeling complex systems, as they help organize responsibilities when designing and implementing different parts of the system in a coordinated and efficient manner.

Strategic Design

Strategic design focuses on defining the high-level architecture and overall organization of the system. It helps define how different parts of the system interact with each other. Key concepts in strategic design include:

  • Bounded context – This is an explicit boundary where a specific domain model is defined and applied.
  • Context mapping – This is a practice that helps understand and define the interactions between different bounded contexts.
  • Context view – This represents a high-level view of how bounded contexts are organized and interact with each other.

Tactical Design

Tactical design focuses on the implementation details within a bounded context. It defines patterns and practices for implementing domain models. Tactical design encompasses the following key concepts:

  • Entities – Objects that have a unique, persistent and meaningful identity over time.
  • Value objects – Objects that are defined by their attributes and have no identity of their own. Examples include addresses, currency, city names and others.
  • Aggregates – Sets of entities and value objects that are treated as a cohesive unit to achieve data consistency. An aggregate has a root entity that is solely responsible for maintaining the integrity of the aggregate.
  • Repositories – Interfaces that provide methods for accessing and manipulating aggregates.
  • Domain Services – Operations that do not belong to any specific entity or value object, but are still part of the domain.

Implementing DDD in an ASP.NET Core Application

In this post, we will focus on the execution of tactical design, starting with the creation of the project and using the main key concepts of tactical design.

To create an application in ASP.NET Core using DDD principles, we will use a fictitious scenario where we need to create a commission module for a web sales system.

In the example, we will use the microservices software architecture design, where an endpoint will be created to process the commission of a sale. This endpoint will receive the sale ID, amount and currency code, make the necessary calculations, and record the commission details in a table.

You can access the complete project code in this GitHub repository e-Sales source code.

To create the sample application, use the following commands:

dotnet new sln -n eSales
cd eSales

These commands will create the Solution project.

Layers in a DDD Microservice Project

Layering is commonly used in medium and large applications with significant business model complexity. Structuring software into layers helps developers manage this complexity, facilitating maintenance, evolution and separation of responsibilities. The most commonly used layers include:

  • Domain layer – Contains the core business logic and domain rules, independent of technical details.
  • Application layer – Orchestrates operations and coordinates interactions between the presentation and domain layers without including business logic.
  • Infrastructure layer – Implements technical details such as data persistence, communication with other services, and external integrations.

This approach modularizes the application, making it more robust and scalable. Below, we will implement three layers in the example application.

DDD microservice structure

1. Domain Layer

In DDD, the domain layer is one of the most important layers and contains the business rules and logic related to the application domain.

This layer is the core of the system and should be isolated from technical details, such as data persistence or user interfaces, so that the business logic remains clear and cohesive.

To create the domain layer in the sample application, run the following commands in a terminal within the directory where the solution project is located.

dotnet new classlib -n Commission.Domain
dotnet sln add Commission.Domain/Commission.Domain.csproj

These commands will create a blank ASP.NET Core classlib project and add it to the eSales solution. Then, open the project with your IDE, and let’s start developing the domain layer.

Inside the Commission.Domain project, create a folder called Entities, and inside it, add the following class:

public class SaleCommission
{
    public Guid Id { get; private set; }
    public decimal Amount { get; private set; }
    public DateTime Date { get; private set; }
    public Guid SaleId { get; private set; }
    public string CurrencyCode { get; private set; }
    public string CommissionType { get; private set; }
    public bool IsProcessed { get; private set; }

    public Currency Currency => new Currency(CurrencyCode);

    private SaleCommission() { }

    public SaleCommission(Guid saleId, decimal amount, DateTime date, Currency currency, string commissionType)
    {
        if (amount <= 0) throw new ArgumentException("Amount must be greater than zero.");
        if (string.IsNullOrEmpty(commissionType)) throw new ArgumentException("CommissionType cannot be null or empty.");

        Id = Guid.NewGuid();
        SaleId = saleId;
        Amount = amount;
        Date = date;
        CurrencyCode = currency.Code;
        CommissionType = commissionType;
        IsProcessed = false;
    }

    public void Process()
    {
        if (IsProcessed) throw new InvalidOperationException("Commission has already been processed.");
        IsProcessed = true;
    }

    public void ApplyRate(decimal rate)
    {
        if (rate <= 0) throw new ArgumentException("Rate must be greater than zero.");
        Amount = Amount * rate;
    }
}

The SaleCommission class represents the sales commission entity and it has several elements that demonstrate that it is in harmony with the DDD principles. Below, we will check them in detail.

  • Calculated properties: Derived property that creates a Currency object based on the CurrencyCode, encapsulating the logic related to the currency. It is a value object (we will create it shortly).
  • Private constructor: The private SaleCommission() { } constructor is declared private to Entity Framework or other ORM tools that require a parameter-less constructor. This helps ensure that the entity can only be created through the explicit constructor, maintaining the integrity of the object and preventing an empty object of the class from being created.
  • Public constructor: The public SaleCommission(Guid saleId, decimal amount, DateTime date, Currency currency, string commissionType) constructor initializes the entity and validates the parameters, so that the object is always in a valid state when created.
  • Methods: The Process() method keeps a commission from being processed more than once, maintaining the consistency of the object’s state. The ApplyRate(decimal rate) method applies a rate to the commission value, allowing additional calculations and changes to the commission value in a controlled and explicit manner.

We can also identify in the SaleCommission class some DDD principles for creating entities such as:

  • Encapsulation: The class encapsulates its business logic and validation, so that the entity is always in a valid state and preventing business rules from being spread elsewhere.
  • Invariants: The validation logic in the constructor and methods (amount > 0, commissionType not null or empty, rate > 0, etc.) maintains the domain invariants, so that the business rules are respected.
  • Rich domain model: Instead of being an anemic class (with only getters and setters), the SaleCommission class has behavior and business logic, making it a rich domain model.
  • Property immutability: Properties such as Id, Amount, Date, SaleId, CurrencyCode, CommissionType and IsProcessed have private setters, ensuring that they can only be changed within the entity itself, preserving data integrity and preventing its state from being modified by an external caller.

Now let’s create the value object Currency used in the SaleCommission class. Create a new folder called ValueObjects and inside it add the class below:

public class Currency
{
    public string Code { get; private set; }

    public Currency(string code)
    {
        if (string.IsNullOrEmpty(code)) throw new ArgumentException("Currency code cannot be null or empty.");
        Code = code;
    }

    public override string ToString() => Code;

    public override bool Equals(object obj) =>
        obj is Currency other && Code == other.Code;

    public override int GetHashCode() => Code.GetHashCode();
}

A value object is a fundamental concept in DDD. It represents a domain concept that is uniquely identified by its set of attributes, rather than by an identity or identifier (ID). Value objects are immutable, compared based on their attributes, and used to describe aspects or characteristics of entities.

In the Currency class we can identify the following characteristics present in value objects:

  • Immutability: The currency code (Code) is defined in the constructor and cannot be changed later.
  • Attribute-based equality: Two Currency objects are equal only if their currency codes (Code) are the same.
  • No identity of their own: Currency is identified by the value of the Code and not by a unique identifier. We can also identify that the Currency class has a constructor that guarantees that the object will always be created in a valid state, throwing an exception if the currency code is null or empty. The only property of Currency (code) is immutable after initialization, so that the value is not changed externally.
  • Overridden methods: ToString() returns the currency code, making representing it in string format easier. Equals(object obj) checks for equality based on the currency code. GetHashCode() returns the hash code based on the currency code, for correct operation on hash-based collections (such as dictionaries and sets).

Repositories

Repositories are a design pattern that provides an abstraction over the data persistence layer.

To maintain the separation of concerns and independence of business logic from infrastructure details, repository interfaces are placed in the domain layer.

The domain layer should focus exclusively on the business logic and rules of the domain, so by defining only repository interfaces in the domain layer, we can keep the domain independent of implementation details such as the database type or persistence technology.

To implement the interfaces, create a new folder called “Repositories” and add in it the following interface:

public interface ICommissionRepository
{
  Task Add(SaleCommission commission);
  Task<SaleCommission> GetById(Guid id);
}

Here we have two methods that will be implemented in the infrastructure layer to add and retrieve items to the database.

Service

In a domain layer, a Service is a design pattern that contains business logic or complex operations that do not naturally belong to an entity or value object. Domain services are typically used to perform operations involving entities, to enforce business rules that cross the boundaries of a single entity, or to execute rules that are present in entities or value objects.

Within the domain project, create a folder called Services and add the following interface and class inside it:

namespace Commission.Domain.Services
{
    public interface ICommissionService
    {
        Task ProcessCommission(Guid saleId, Currency currency, decimal saleAmount);
    }
}
public class CommissionService : ICommissionService
{
    private readonly ICommissionRepository _commissionRepository;

    public CommissionService(ICommissionRepository commissionRepository)
    {
        _commissionRepository = commissionRepository;
    }

    public async Task ProcessCommission(Guid saleId, Currency currency, decimal saleAmount)
    {
        if (saleAmount <= 0) throw new ArgumentException("Sale amount must be greater than zero.", nameof(saleAmount));

        var commission = new SaleCommission(saleId, saleAmount, DateTime.Now, currency, "Standard");

        decimal baseRate = 0.05m;

        commission.ApplyRate(baseRate);

        commission.Process();

        await _commissionRepository.Add(commission);
    }
}

Note that the CommissionService class defines the ProcessCommission(Guid saleId, Currency currency, decimal saleAmount) method that applies validations and then saves the entity to the database.

In this class, we can see that the commission calculation and processing logic are centralized in a single location, promoting code reuse and clarity. In addition, the coordination between commission creation, fee application, processing and persistence is clear, providing a clean separation of responsibilities and encapsulating complex business logic that crosses the boundaries of individual entities.

2. Infrastructure Layer

The infrastructure layer contains the concrete implementations of the persistence mechanisms (databases) and communication with external systems such as API clients and others. It encapsulates technical details that should not be exposed in the domain or application layers. This layer facilitates the separation of concerns and allows the domain to remain independent of technology and implementation details.

To create the Infrastructure application, in the application root directory run the following commands:

dotnet new classlib -n Commission.Infrastructure

dotnet sln add Commission.Infrastructure/Commission.Infrastructure.csproj

The first command creates the infrastructure project, and the second command adds the project to the solution.

Now, within the infrastructure project, run the following commands in the terminal to install the dependencies of the NuGet packages that will be used to manage database operations with Entity Framework Core.

dotnet add package Microsoft. EntityFrameworkCore
dotnet add package Microsoft. EntityFrameworkCore. Design
dotnet add package Microsoft. EntityFrameworkCore. SqlServer

Then, create a new folder called “Data” and inside it create the class below:

namespace Commission.Infrastructure.Data;

public class CommissionDbContext : DbContext
{
    public DbSet<SaleCommission> SaleCommissions { get; set; }

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

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<SaleCommission>()
            .Property(sc => sc.CurrencyCode)
            .HasColumnName("CurrencyCode")
            .HasColumnType("nvarchar(3)");

        base.OnModelCreating(modelBuilder);
    }
}

Here we have the class that maps the SaleCommissions entity with EF Core, it also has a method for converting the SaleCommission type to the CurrencyCode column when the EF Core migrations commands are executed.

The next step is to create the repository class that will implement the methods defined in the interface defined in the domain layer. So, create a new folder called “Repositories” and add the following class to it:

public class CommissionRepository : ICommissionRepository
{
    private readonly CommissionDbContext _context;

    public CommissionRepository(CommissionDbContext context)
    {
        _context = context;
    }

    public async Task Add(SaleCommission commission)
    {
       await _context.SaleCommissions.AddAsync(commission);
       await _context.SaveChangesAsync();
    }

    public async Task<SaleCommission> GetById(Guid id)
    {
        return await _context.SaleCommissions.FindAsync(id);
    }
}

Note that here the CommissionRepository class uses the SaleCommission class that is part of the domain, so you need to add the domain dependency to the infrastructure layer.

The repository layer is ready. For the scenario in the post it is simple, as it only interacts with the database, but in more complex scenarios the repository layer is responsible for the integration with any external system such as web APIs and other services.

3. API (Application) Layer

The application layer orchestrates calls to domain services to meet the needs of use cases. In the context of .NET, in a project that uses a microservices architecture, the application layer is commonly implemented as an ASP.NET Core Web API project.

The primary function of the application layer is to coordinate tasks and not interfere with the state of the domain because this is the responsibility of the domain layer itself, which has its business rules directly coupled to entity classes, value objects and domain services. There should be no business rules or domain-related rules in this layer.

Other responsibilities of the application layer include exposing HTTP endpoints that allow interaction with the microservice’s business logic, as well as handling the lifecycle of HTTP requests, including authentication, authorization, validation and response handling.

So, to create the application layer in your project, run the following commands in the project root:

dotnet new webapi -n Commission.API
dotnet sln add Commission.API/Commission.API.csproj

Then run the following commands to add the other layers’ reference to it:

cd Commission.API
dotnet add reference ../Commission.Domain/Commission.Domain.csproj
dotnet add reference ../Commission.Infrastructure/Commission.Infrastructure.csproj

Inside the API project, create a new folder called “Models” and inside it add the class below:

public class ProcessCommissionRequest
{
    public Guid SaleId { get; set; }
    public decimal SaleAmount { get; set; }
    public Currency Currency { get; set; }
}

This class is just a Data Transfer Object (DTO) that will be used to receive the requested data and pass it on to the service.

Create a new folder called “Controllers” and inside it create the controller below:

[ApiController]
[Route("api/[controller]")]
public class CommissionController : ControllerBase
{
    private readonly ICommissionService _commissionService;

    public CommissionController(ICommissionService commissionService)
    {
        _commissionService = commissionService;
    }

    [HttpPost]
    [Route("process-commission")]
    public async Task<IActionResult> ProcessCommission([FromBody] ProcessCommissionRequest request)
    {
       await _commissionService.ProcessCommission(request.SaleId, request.Currency, request.SaleAmount);
       return Ok();
    }
}

Here we have an endpoint to process the commission that receives the request data as a parameter and sends it to the service.

Now in the Program class, replace the existing code with the code below:

using Commission.Domain.Repositories;
using Commission.Domain.Services;
using Commission.Infrastructure.Data;
using Commission.Infrastructure.Repositories;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddDbContext<CommissionDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"), 
	sqlOptions => sqlOptions.MigrationsAssembly("Commission.Infrastructure")));

builder.Services.AddScoped<ICommissionRepository, CommissionRepository>();
builder.Services.AddScoped<ICommissionService, CommissionService>();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
	app.UseSwagger();
	app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseRouting();
app.UseEndpoints(endpoints =>
{
    endpoints.MapControllers();
});

app.Run();

Here we are configuring the classes and interfaces used in the application, we also configure the database connection string, and we inform EF Core that the files related to migrations are in the Commission.Infrastructure project.

To set the connection string value, just add the following code in the file appsettings.json:

"ConnectionStrings": {
  "DefaultConnection": "Server=YOUR_LOCAL_CONNECTION;Database=SalesDb;Trusted_Connection=True;"
},

The last step is to run the migrations commands and generate the database and tables. So in the root of the project, open a terminal and run the following commands:

dotnet ef migrations add InitialCreate --project Commission.Infrastructure --startup-project Commission.API
dotnet ef database update --project Commission.Infrastructure --startup-project Commission.API

To verify that the application is functional, simply run the Commission.API project and request the process-commission endpoint. Here it is using Progress Telerik Fiddler Everywhere:

Request by Fiddler

Checking the data

Dependency Between Layers

When implementing systems using DDD principles, the application layer depends on the domain and the infrastructure, and the infrastructure depends on the domain. However, the domain does not depend on any layer.

The domain layer is the application’s core and represents the system’s business logic, rules and concepts. Independence from other layers helps keep the business logic pure and isolated, allowing it to evolve without being affected by changes in other parts of the system.

Dependencies between layers

Conclusion and Final Considerations

Domain-driven design is an approach to creating software based on the business domain, which includes all the complexities that can be found in small, medium and large companies.

Understanding these complexities and solving their problems with technology is perhaps the biggest challenge for any developer. But, as discussed at the beginning of this post, you don’t need to understand all the nuances of DDD at once, you can start slowly and adopt principles as your understanding evolves.

In this post, we had a brief overview of DDD and implemented a microservice in ASP.NET Core, separating the main responsibilities of each of the three layers covered: domain, infrastructure and application.

Therefore, whenever possible, consider adopting DDD to create systems that are more aligned with business logic and prepared to deal with complexity effectively.


assis-zang-bio
About the Author

Assis Zang

Assis Zang is a software developer from Brazil, developing in the .NET platform since 2017. In his free time, he enjoys playing video games and reading good books. You can follow him at: LinkedIn and Github.

Related Posts

Comments

Comments are disabled in preview mode.