Domain-Driven Design (DDD)
Domain-Driven Design (DDD) is an approach to software development that centers the design around the business domain. Introduced by Eric Evans, it provides vocabulary and patterns for tackling complex domains.
Strategic Design
Section titled βStrategic DesignβBounded Context
Section titled βBounded ContextβA Bounded Context is an explicit boundary within which a domain model applies. The same term can mean different things in different contexts.
βββββββββββββββββββββββ ββββββββββββββββββββββββ Order Context β β Shipping Context ββ β β ββ Customer: buyer β β Customer: recipientββ Product: item β β Product: package ββ Order: purchase β β Order: shipment ββββββββββββββββββββββββ βββββββββββββββββββββββUbiquitous Language
Section titled βUbiquitous LanguageβUse the same terms everywhere β in conversations, code, tests, and documentation. If the business says βfulfil an orderβ, the code should say order.Fulfil(), not order.UpdateStatus(OrderStatus.Fulfilled).
Tactical Design Building Blocks
Section titled βTactical Design Building BlocksβHas a unique identity that persists over time. Two entities with the same attributes are still different if their IDs differ.
public class Order{ public Guid Id { get; private set; } public OrderStatus Status { get; private set; } private readonly List<OrderLine> _lines = new();
public Order(Guid customerId) { Id = Guid.NewGuid(); CustomerId = customerId; Status = OrderStatus.Draft; }
public void AddLine(ProductId productId, Money price, int quantity) { if (Status != OrderStatus.Draft) throw new InvalidOperationException("Cannot modify a submitted order"); _lines.Add(new OrderLine(productId, price, quantity)); }
public void Submit() { if (!_lines.Any()) throw new InvalidOperationException("Cannot submit an empty order"); Status = OrderStatus.Submitted; AddDomainEvent(new OrderSubmitted(Id)); }}Value Object
Section titled βValue ObjectβDefined by its attributes, not identity. Immutable. Two value objects with the same values are equal.
public record Money(decimal Amount, string Currency){ public Money Add(Money other) { if (Currency != other.Currency) throw new InvalidOperationException("Currency mismatch"); return new Money(Amount + other.Amount, Currency); }
public Money Multiply(int factor) => new(Amount * factor, Currency);}
public record Address(string Street, string City, string PostalCode, string Country);
public record ProductId(Guid Value){ public static ProductId New() => new(Guid.NewGuid());}Aggregate
Section titled βAggregateβA cluster of entities and value objects treated as a single unit. The Aggregate Root is the only entry point β external code never holds references to inner entities.
// Order is the aggregate rootpublic class Order // Aggregate Root{ public Guid Id { get; private set; } private readonly List<OrderLine> _lines = new(); // inner entities
// All modifications go through the root public void AddLine(ProductId productId, Money unitPrice, int qty) { var existing = _lines.FirstOrDefault(l => l.ProductId == productId); if (existing is not null) existing.IncreaseQuantity(qty); else _lines.Add(new OrderLine(productId, unitPrice, qty)); }
public Money Total => _lines.Aggregate( Money.Zero("GBP"), (sum, line) => sum.Add(line.LineTotal));}
// OrderLine is an inner entity β never accessed directly from outsideinternal class OrderLine{ internal ProductId ProductId { get; private set; } internal Money UnitPrice { get; private set; } internal int Quantity { get; private set; } internal Money LineTotal => UnitPrice.Multiply(Quantity);
internal void IncreaseQuantity(int amount) => Quantity += amount;}Repository
Section titled βRepositoryβProvides collection-like access to aggregates. Hides persistence details from the domain.
// Interface in the domain layerpublic interface IOrderRepository{ Task<Order?> GetByIdAsync(Guid id, CancellationToken ct = default); Task AddAsync(Order order, CancellationToken ct = default); Task<IReadOnlyList<Order>> GetByCustomerAsync(Guid customerId, CancellationToken ct = default);}
// Implementation in the infrastructure layerpublic class EfOrderRepository : IOrderRepository{ private readonly AppDbContext _db;
public EfOrderRepository(AppDbContext db) => _db = db;
public Task<Order?> GetByIdAsync(Guid id, CancellationToken ct) => _db.Orders.Include("_lines").FirstOrDefaultAsync(o => o.Id == id, ct);
public async Task AddAsync(Order order, CancellationToken ct) { await _db.Orders.AddAsync(order, ct); await _db.SaveChangesAsync(ct); }}Domain Service
Section titled βDomain ServiceβLogic that doesnβt naturally belong to a single entity or value object:
public class PricingService{ private readonly IDiscountRepository _discounts;
public PricingService(IDiscountRepository discounts) => _discounts = discounts;
public async Task<Money> CalculateTotalAsync(Order order, Guid customerId) { var baseTotal = order.Total; var discount = await _discounts.GetActiveDiscountForCustomerAsync(customerId); return discount is null ? baseTotal : discount.Apply(baseTotal); }}Folder Structure
Section titled βFolder Structureβsrc/βββ Domain/ # No external dependenciesβ βββ Orders/β β βββ Order.csβ β βββ OrderLine.csβ β βββ OrderStatus.csβ β βββ IOrderRepository.csβ βββ Shared/β βββ Money.csβ βββ Entity.csβββ Application/ # Depends on Domain onlyβ βββ Orders/β βββ PlaceOrderCommand.csβ βββ PlaceOrderHandler.csβββ Infrastructure/ # Depends on Application + Domain βββ Persistence/ βββ EfOrderRepository.csWhen to Use DDD
Section titled βWhen to Use DDDβGood fit for:
- Complex business logic with many rules and workflows
- Large teams needing clear domain ownership
- Long-lived systems where the domain evolves over years
Overkill for:
- Simple CRUD applications
- Data pipelines or reporting tools
- Prototypes and MVPs