ASP.NET Core  

Building Domain-Driven Design (DDD) APIs in ASP.NET Core

1. Introduction

As software systems grow in complexity, managing business rules, scalability, and maintainability becomes a challenge.
Traditional layered architectures often result in anemic models—where business logic gets scattered between controllers and services.

Domain-Driven Design (DDD) helps solve this by structuring your application around the business domain, making your code more maintainable and closer to real-world business processes.

In this article, we’ll learn how to design and build clean, scalable APIs using ASP.NET Core 9 with DDD principles, including:

  • Entities, Value Objects, Aggregates

  • Domain Services and Repositories

  • Infrastructure and Application Layers

  • Handling Commands and Events

  • Example Implementation and folder structure

2. What Is Domain-Driven Design?

Domain-Driven Design (DDD) is a software design approach introduced by Eric Evans.
It focuses on the core domain and domain logic rather than technical layers.

In DDD:

  • You model the real business directly in the code.

  • The domain model is the heart of the application.

  • The application grows from the business language, not just CRUD operations.

3. Core DDD Building Blocks

ConceptDescription
EntityAn object with an identity that persists over time (e.g., Customer, Order).
Value ObjectImmutable object identified by its attributes (e.g., Money, Address).
AggregateA cluster of entities and value objects treated as a single unit.
RepositoryProvides abstraction for retrieving and saving aggregates.
Domain ServiceEncapsulates domain logic that doesn’t belong to a specific entity.
Application ServiceOrchestrates operations, coordinates between domain objects.

4. Architecture Overview

A clean DDD-based ASP.NET Core API typically has these layers:

┌────────────────────────────┐
│        Presentation        │
│ (Controllers, DTOs, API)   │
└────────────┬───────────────┘
             │
┌────────────▼───────────────┐
│      Application Layer      │
│ (Use Cases, Commands, DTOs) │
└────────────┬───────────────┘
             │
┌────────────▼───────────────┐
│        Domain Layer         │
│ (Entities, Services, Logic) │
└────────────┬───────────────┘
             │
┌────────────▼───────────────┐
│     Infrastructure Layer    │
│ (EF Core, DB, Repositories) │
└────────────────────────────┘

This separation ensures high cohesion within each layer and low coupling between layers.

5. Technical Workflow

Technical Workflow: Request to Domain

Client → API Controller → Application Service → Domain Layer (Entities/Logic)
                ↓
        Infrastructure Repository (EF Core)
                ↓
              Database

This ensures:

  • Controllers only handle HTTP.

  • Application services handle orchestration.

  • Domain entities handle core business logic.

6. Example Use Case: Order Management System

Let’s implement a small Order Management API using DDD.

Folder Structure

/OrderManagement
│
├── Application
│   ├── DTOs
│   ├── Services
│   └── Commands
│
├── Domain
│   ├── Entities
│   ├── ValueObjects
│   ├── Services
│   └── Repositories
│
├── Infrastructure
│   ├── Data
│   └── Repositories
│
└── API
    ├── Controllers
    └── Startup.cs

7. Domain Layer

Entity: Order.cs

namespace OrderManagement.Domain.Entities
{
    public class Order
    {
        public Guid Id { get; private set; }
        public string CustomerName { get; private set; }
        private readonly List<OrderItem> _items = new();

        public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();

        public Order(string customerName)
        {
            Id = Guid.NewGuid();
            CustomerName = customerName;
        }

        public void AddItem(string product, int quantity, decimal price)
        {
            var item = new OrderItem(product, quantity, price);
            _items.Add(item);
        }

        public decimal GetTotalAmount()
        {
            return _items.Sum(x => x.Total);
        }
    }
}

Value Object: OrderItem.cs

namespace OrderManagement.Domain.Entities
{
    public class OrderItem
    {
        public string Product { get; }
        public int Quantity { get; }
        public decimal Price { get; }
        public decimal Total => Quantity * Price;

        public OrderItem(string product, int quantity, decimal price)
        {
            Product = product;
            Quantity = quantity;
            Price = price;
        }
    }
}

8. Repository Interface

using OrderManagement.Domain.Entities;

namespace OrderManagement.Domain.Repositories
{
    public interface IOrderRepository
    {
        Task<Order?> GetByIdAsync(Guid id);
        Task AddAsync(Order order);
        Task SaveChangesAsync();
    }
}

9. Infrastructure Layer

EF Core Repository Implementation

using Microsoft.EntityFrameworkCore;
using OrderManagement.Domain.Entities;
using OrderManagement.Domain.Repositories;

namespace OrderManagement.Infrastructure.Data
{
    public class OrderRepository : IOrderRepository
    {
        private readonly AppDbContext _context;

        public OrderRepository(AppDbContext context)
        {
            _context = context;
        }

        public async Task<Order?> GetByIdAsync(Guid id)
        {
            return await _context.Orders
                .Include(o => o.Items)
                .FirstOrDefaultAsync(o => o.Id == id);
        }

        public async Task AddAsync(Order order)
        {
            await _context.Orders.AddAsync(order);
        }

        public async Task SaveChangesAsync()
        {
            await _context.SaveChangesAsync();
        }
    }
}

10. Application Layer

Application Service: OrderService.cs

using OrderManagement.Domain.Entities;
using OrderManagement.Domain.Repositories;

namespace OrderManagement.Application.Services
{
    public class OrderService
    {
        private readonly IOrderRepository _orderRepo;

        public OrderService(IOrderRepository orderRepo)
        {
            _orderRepo = orderRepo;
        }

        public async Task<Guid> CreateOrderAsync(string customerName, List<(string, int, decimal)> items)
        {
            var order = new Order(customerName);
            foreach (var (product, qty, price) in items)
                order.AddItem(product, qty, price);

            await _orderRepo.AddAsync(order);
            await _orderRepo.SaveChangesAsync();

            return order.Id;
        }

        public async Task<Order?> GetOrderAsync(Guid id)
        {
            return await _orderRepo.GetByIdAsync(id);
        }
    }
}

11. Presentation Layer (API)

Controller: OrderController.cs

using Microsoft.AspNetCore.Mvc;
using OrderManagement.Application.Services;

namespace OrderManagement.API.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class OrderController : ControllerBase
    {
        private readonly OrderService _orderService;

        public OrderController(OrderService orderService)
        {
            _orderService = orderService;
        }

        [HttpPost]
        public async Task<IActionResult> Create([FromBody] CreateOrderRequest request)
        {
            var id = await _orderService.CreateOrderAsync(
                request.CustomerName, 
                request.Items.Select(i => (i.Product, i.Quantity, i.Price)).ToList()
            );
            return Ok(new { OrderId = id });
        }

        [HttpGet("{id}")]
        public async Task<IActionResult> Get(Guid id)
        {
            var order = await _orderService.GetOrderAsync(id);
            return order == null ? NotFound() : Ok(order);
        }
    }

    public class CreateOrderRequest
    {
        public string CustomerName { get; set; } = "";
        public List<OrderItemRequest> Items { get; set; } = new();
    }

    public class OrderItemRequest
    {
        public string Product { get; set; } = "";
        public int Quantity { get; set; }
        public decimal Price { get; set; }
    }
}

12. Key Benefits of DDD in ASP.NET Core

BenefitExplanation
MaintainabilityEach layer has a clear purpose; domain logic is isolated.
TestabilityYou can unit test domain logic without web or DB dependencies.
ScalabilityAdding new features or modules doesn’t break others.
Ubiquitous LanguageCode matches business terms (Order, Customer, Invoice).
ExtensibilityDomain model easily evolves as business grows.

13. Best Practices

  1. Keep Entities Clean — avoid adding persistence logic directly in entities.

  2. Use Value Objects for immutable domain data.

  3. Use Domain Events to decouple cross-domain communication.

  4. Repositories should return Aggregates, not raw entities.

  5. Unit Test the Domain Layer independently.

  6. Avoid circular dependencies between layers.

  7. Use CQRS pattern for large-scale APIs to separate reads/writes.

14. Example: Domain Event Handling

public class OrderCreatedEvent
{
    public Guid OrderId { get; }
    public DateTime CreatedAt { get; }

    public OrderCreatedEvent(Guid orderId)
    {
        OrderId = orderId;
        CreatedAt = DateTime.UtcNow;
    }
}

A DomainEventDispatcher service can then publish this event to notify other bounded contexts, such as Inventory or Billing.

15. Testing the Domain Layer

Example unit test for domain logic:

[Fact]
public void Order_TotalAmount_ShouldBeCalculatedCorrectly()
{
    var order = new Order("Rajesh Gami");
    order.AddItem("Mouse", 2, 500);
    order.AddItem("Keyboard", 1, 1500);

    Assert.Equal(2500, order.GetTotalAmount());
}

This ensures business rules are correct without database or controller dependencies.

16. Common Mistakes to Avoid

  • Treating DDD as a simple layered architecture (it’s more conceptual).

  • Overusing abstractions for small projects.

  • Mixing persistence models (Entity Framework) with domain entities.

  • Writing anemic models — entities with only getters/setters.

  • Forgetting to align model names with real business terms.

17. Conclusion

Domain-Driven Design (DDD) is not just an architecture, it’s a way of thinking.
When used with ASP.NET Core, it provides a clean, modular, and testable approach to building complex business applications.

By focusing on the core domain, using entities, value objects, and repositories, and keeping business rules inside the domain layer, your APIs become much more robust, maintainable, and future-ready.