Tuesday, November 18, 2025

Architecture - Some Important Concepts of DDD

1. Domain Events

Domain events model things that happen inside the domain and allow other parts of the system to react.

Use cases

  • Send email after order placed
  • Reduce inventory
  • Publish message to message bus
  • Update read models

Domain Event Example

public class OrderPlacedEvent : IDomainEvent { public Guid OrderId { get; } public DateTime PlacedAt { get; } public OrderPlacedEvent(Guid orderId) { OrderId = orderId; PlacedAt = DateTime.UtcNow; } }

Aggregate raising event

public class Order : AggregateRoot { public List<OrderItem> Items { get; private set; } public void Place() { // business logic... AddDomainEvent(new OrderPlacedEvent(Id)); } }

Domain Event Handler

public class OrderPlacedHandler : IHandler<OrderPlacedEvent> { public Task Handle(OrderPlacedEvent domainEvent) { // Example action Console.WriteLine($"Order {domainEvent.OrderId} placed!"); return Task.CompletedTask; } }

2. Ubiquitous Language

It means:

Everyone (developers, business, testers, PMs) uses the SAME terms with SAME meaning.

Example:
Business says Order → OrderItem → Payment → Customer
So your code should also use Order, not “Tbl_Order”.

Bad example:

  • Business says “Order”

  • Code says “PurchaseTransactionRecord”

Good example:

public class Order { } public class OrderItem { }

This maintains clarity and reduces misunderstandings.


3. Bounded Context

A bounded context defines a logical boundary within which a domain model is consistent.

Example:

In an E-commerce system:

Bounded ContextMeaning of “Order”
Ordering BCOrder = items customer wants
Billing BCOrder = financial transaction
Shipping BCOrder = package to be shipped

Each BC has its own:

  • Model
  • Database
  • Services
  • Ubiquitous language

C# Example

Different BCs have different Order definitions:

Ordering BC

public class Order { public Guid Id { get; } public List<OrderItem> Items { get; } }

Shipping BC

public class OrderShipment { public Guid OrderId { get; } public string TrackingNumber { get; private set; } }

4. DTO (Data Transfer Object)

DTOs are simple objects used to transfer data across boundaries (API layer ↔ Application layer).

Not used for business logic.

Example

public class OrderDto { public Guid Id { get; set; } public decimal TotalAmount { get; set; } }

Controller returning DTO instead of domain entity

[HttpGet("{id}")] public async Task<ActionResult<OrderDto>> GetOrder(Guid id) { var order = await _orderService.GetOrder(id); return new OrderDto { Id = order.Id, TotalAmount = order.TotalAmount }; }

5. Aggregate

Aggregate = cluster of domain objects treated as a single unit
Aggregate Root = main entry point, enforces invariants.

Example

Order (root) → contains OrderItems (child entities)

public class Order : AggregateRoot { private readonly List<OrderItem> _items = new(); public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly(); public void AddItem(Guid productId, int qty, decimal price) { _items.Add(new OrderItem(productId, qty, price)); } }

Child entity:

public class OrderItem { public Guid ProductId { get; } public int Quantity { get; private set; } public decimal Price { get; private set; } internal OrderItem(Guid productId, int qty, decimal price) { ProductId = productId; Quantity = qty; Price = price; } }

Rules:

  • Only aggregate root is loaded from repository.
  • Only aggregate root is saved.
  • External code cannot modify inner entities directly.


6. Value Object

Value objects:

  • Immutable

  • Compared by values (not identity)

  • No Id

  • Represent concepts like Money, Email, Address, Quantity

C# Example

public class Email : ValueObject { public string Value { get; } public Email(string email) { if (!email.Contains("@")) throw new Exception("Invalid email"); Value = email; } protected override IEnumerable<object> GetEqualityComponents() { yield return Value; } }

Usage:

var userEmail = new Email("test@gmail.com");

7. Rich Model vs Anemic Model

Anemic Model (Anti-pattern)

  • Entities only have properties
  • Logic sits outside in service classes
  • Looks like a database table
  • Easy but poor domain modeling

Example:

public class Order { public Guid Id { get; set; } public decimal Total { get; set; } }

Business logic in services:

public decimal CalculateTotal(Order order) { return order.Items.Sum(i => i.Price); }

Rich Domain Model (Recommended in DDD)

Business logic lives inside domain entities.

Example:

public class Order { private readonly List<OrderItem> _items = new(); public void AddItem(Guid productId, int qty, decimal price) { if (qty <= 0) throw new Exception("Qty must be > 0"); _items.Add(new OrderItem(productId, qty, price)); } public decimal GetTotal() => _items.Sum(i => i.Price * i.Quantity); }

Rich model:

  • Enforces business rules
  • Reduces duplication
  • Maintains invariants


🎯 Summary Table

ConceptOne-line definition
RepositoryAbstraction over data access for aggregates
Domain EventsThings that happened in the domain
Ubiquitous LanguageSame terms used across business & code
Bounded ContextClear boundary where a domain model is consistent
DTOData-only object for transferring data
AggregateCluster of domain objects with rules
Value ObjectImmutable concept compared by value
Rich ModelEntities with behavior (preferred in DDD)
Anemic ModelEntities with only properties (anti-pattern)




No comments:

Post a Comment

CI/CD - Safe DB Changes/Migrations

Safe DB Migrations means updating your database schema without breaking the running application and without downtime . In real systems (A...