Domain-Driven Design (DDD): A Comprehensive Overview
Domain-Driven Design (DDD) approaches software development by emphasizing underlying business domains. First coined by Eric Evans in 2003, DDD offers a set of principles and patterns to help developers create complex software systems closely aligned with a business’ needs.
History of Domain-Driven Design
One can trace the origins of DDD to the early 2000s when software systems became increasingly complex and traditional design methodologies proved insufficient for tackling such intricacies.
Evans recognized the need for greater business-centric approaches to bridge the gap between technical design and business requirements. His work would later form the basis for DDD, which has become widely adopted in the enterprise software development sector.
Domain-Driven Design Necessities
DDD is highly beneficial in scenarios with:
Complexity, where business domains are complicated and require deep understanding and accurate modeling.
Alignment, where there is an essential need for close alignment between software systems and business objectives.
Collaboration, where there is a strong need for effective cooperation between domain experts and developers.
Evolution, where systems are expected to evolve with changing business requirements over time.
Key Concepts in Domain-Driven Design
Domain Entities are objects that have a distinct identity and life cycle and are often mutable, representing a thread of continuity in a system.
For example, in banking applications, entities could consist of customers with an identity (customer ID) which remains constant, even as other customer attributes (address, phone number, etc.) change.
Value Objects are immutable objects representing a descriptive aspect of the domain, without a distinct identity, compared based on their attributes rather than identities.
With the banking app example, an address could become a value object, but two addresses would be considered equal if their properties (street, city, postcode) were the same.
Aggregates are clusters of entities and value objects treated as a single unit for data changes. These have a root entity, or aggregate root, which enforces the consistency of changes within the aggregate.
For instance, an Order aggregate could consist of the Order entity as the aggregate root and several OrderItem value objects, with order changes going through the Order entity.
Domain Services are operations that do not naturally fit within an entity or value object but still belong to the domain model. These often encapsulate business logic involving multiple entities or aggregates. This can include operations like a CurrencyConversionService that converts amounts between different currencies.
Domain-Driven Design: Pros vs Cons
Some of the pros of DDD can include:
Ensuring that software models closely align with business requirements, strengthening communication and collaboration.
Tackling complexity concerns with a structured approach in large systems.
Promoting a clean, maintainable codebase that is easier to understand and evolves over time
However, the cons of DDD often:
Creates steep learning curves that require developers to have extensive knowledge of its technical and business aspects
Introduces issues with unnecessary complexity and overhead to smaller, less complex systems
Requires significant and resource-intensive collaboration with domain experts
Orchestration in Domain Services
Domain service orchestration often involves coordinating interactions between multiple domain entities and services to fulfill business processes. In DDD, orchestration can help domain services manage complex workflows spanning multiple aggregates or services.
An example in C# would be as follows:
public class OrderProcessingService
{
private readonly IOrderRepository _orderRepository;
private readonly IInventoryService _inventoryService;
private readonly IPaymentService _paymentService;
public OrderProcessingService(IOrderRepository orderRepository, IInventoryService inventoryService, IPaymentService paymentService)
{
_orderRepository = orderRepository;
_inventoryService = inventoryService;
_paymentService = paymentService;
}
public void ProcessOrder(Guid orderId)
{
var order = _orderRepository.GetById(orderId);
if (_inventoryService.ReserveItems(order))
{
if (_paymentService.Charge(order))
{
order.MarkAsCompleted();
_orderRepository.Save(order);
}
else
{
order.MarkAsPaymentFailed();
}
}
else
{
order.MarkAsOutOfStock();
}
_orderRepository.Save(order);
}
}
In this example, OrderProcessingService orchestrates the process of reserving inventory and charging customers for orders. This orchestration fits well for a domain service as it involves multiple steps that interact with different parts of the domain model.
Domain-Driven Design with Clean Architecture
Popularized by Robert C. Martin, clean architecture is an architectural pattern that emphasizes the separation of concerns and independence from frameworks and databases. It is highly compatible with DDD by encouraging domain-centric approaches.
Key Layers in Clean Architecture:
Entities represent the core business logic and domain objects, including entities and value objects in DDD.
Use cases encapsulate application-specific business rules, which are often services that orchestrate domain logic and coordinate between repositories, services, and other dependencies.
Interface adapters convert data from external sources like controllers and user interfaces (UIs) into formats expected by use cases.
Infrastructure contains implementation details like database access, external services, and frameworks.
Here’s an example of Domain-Centric Clean Architecture in C#:
// Domain Layer
public class Customer : Entity
{
public string Name { get; private set; }
public Address Address { get; private set; }
public Customer(string name, Address address)
{
Name = name;
Address = address;
}
// Domain logic here
}
// Application Layer (Use Cases)
public class CreateOrderUseCase
{
private readonly IOrderRepository orderRepository;
private readonly ICustomerRepository customerRepository;
public CreateOrderUseCase(IOrderRepository orderRepository, ICustomerRepository customerRepository)
{
orderRepository = orderRepository;
customerRepository = customerRepository;
}
public void Execute(CreateOrderCommand command)
{
var customer = customerRepository.GetById(command.CustomerId);
var order = new Order(customer, command.Items);
orderRepository.Save(order);
}
}
// Infrastructure Layer
public class OrderRepository : IOrderRepository
{
private readonly DatabaseContext context;
public OrderRepository(DatabaseContext context)
{
context = context;
}
public void Save(Order order)
{
context.Orders.Add(order);
context.SaveChanges();
}
}
In this setup, the domain layer is completely independent of infrastructure concerns, allowing the core logic to remain clean and focused. Use cases also act as orchestrators to coordinate between domain entities and repositories.
Conclusion
Domain-driven design offers powerful methodologies for tackling complex business domains. Combined with clean architecture, developers can leverage DDD to build robust, maintainable systems closely aligned with business needs.
Although DDD requires significant investments in terms of learning and collaboration, it provides substantial benefits such as clarity, maintenance, and scalability, especially in complex and evolving domains.
Subscribe to my newsletter
Read articles from Muhammad Rizwan directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by