Domain Driven Design Fundamentals
Domain-Driven Design (DDD) is a strategic approach to software design that prioritizes the domain and its logic, emphasizing collaboration between domain experts and developers.
Originated by Eric Evans, DDD helps manage complex domains by aligning the software model with the business needs.
In this article, we will explore the fundamentals of Domain-Driven Design, starting from the core concepts, moving through practical examples, and demonstrating how to implement these ideas in C#.
Table of Contents
What is Domain-Driven Design?
Domain-Driven Design is a software development methodology that emphasizes creating a rich, comprehensive model of the problem domain.
The goal is to align the software closely with business needs, ensuring that developers and domain experts collaborate effectively to create a shared understanding.
In DDD, the key is to prioritize the "domain" — the business problem that the software is trying to solve — and build a model that represents it as accurately as possible.
This involves building relationships and defining behaviors through continuous interaction between the software and domain experts.
Join 10.000+ subscribers! Stay in the loop with everything you need to know about .NET and Software Development
?? If you like this content, please, subscribe, comment and share
The Building Blocks of DDD
Entities
Entities represent the core objects of the domain that have a distinct identity that persists over time.
An entity is uniquely identifiable, and its identity remains constant even if its attributes change.
Example in C#
public class Customer
{
public Guid Id { get; private set; }
public string Name { get; private set; }
public string Email { get; private set; }
public Customer(Guid id, string name, string email)
{
Id = id;
Name = name;
Email = email;
}
public void ChangeEmail(string newEmail)
{
Email = newEmail;
}
}
In the above example, Customer is an entity because it has a unique identifier (Id) that distinguishes it from other customers, regardless of changes to Name or Email.
Value Objects
Value objects represent concepts that do not have a distinct identity. They are defined by their attributes rather than by a unique identifier, and they are immutable.
Example in C#
public class Address
{
public string Street { get; private set; }
public string City { get; private set; }
public string PostalCode { get; private set; }
public Address(string street, string city, string postalCode)
{
Street = street;
City = city;
PostalCode = postalCode;
}
}
Address is a value object. If you change any property of an address, it becomes a new address altogether.
Value objects should be treated as a complete unit, and operations on them should always result in new instances.
Aggregates and Aggregate Roots
An aggregate is a cluster of entities and value objects that function as a single unit. The aggregate root is the main entry point for interacting with the aggregate, enforcing consistency rules.
Example in C#
public class Order
{
public Guid Id { get; private set; }
public List<OrderItem> Items { get; private set; } = new List<OrderItem>();
public DateTime OrderDate { get; private set; }
public Order(Guid id)
{
Id = id;
OrderDate = DateTime.Now;
}
public void AddItem(OrderItem item)
{
Items.Add(item);
}
}
public class OrderItem
{
public Guid ProductId { get; private set; }
public int Quantity { get; private set; }
public OrderItem(Guid productId, int quantity)
{
ProductId = productId;
Quantity = quantity;
}
}
Order is an aggregate root, and OrderItem is part of the aggregate. You interact with Order to modify items, ensuring that all rules are consistently enforced at the root level.
Repositories
Repositories provide access to aggregates. They encapsulate the logic for retrieving and storing aggregates and provide a convenient API for interacting with them.
Example in C#
public interface IOrderRepository
{
Order GetById(Guid id);
void Save(Order order);
}
public class OrderRepository : IOrderRepository
{
private readonly List<Order> _orders = new List<Order>();
public Order GetById(Guid id)
{
return _orders.FirstOrDefault(o => o.Id == id);
}
public void Save(Order order)
{
_orders.Add(order);
}
}
Repositories like OrderRepository abstract the persistence logic, allowing you to interact with aggregates without exposing the underlying storage details.
Services
Domain services handle operations that are not naturally part of an entity or value object. They encapsulate domain logic that cannot be appropriately placed in an existing entity or value object.
Example in C#
public class OrderService
{
private readonly IOrderRepository _orderRepository;
public OrderService(IOrderRepository orderRepository)
{
_orderRepository = orderRepository;
}
public void PlaceOrder(Order order)
{
// Some business logic, such as applying discounts or validating items
_orderRepository.Save(order);
}
}
OrderService operates on entities (Order) but isn’t itself part of any aggregate. It holds business logic that doesn’t naturally belong in Order or OrderItem.
Ubiquitous Language
Ubiquitous Language is the common language shared by developers and domain experts. It ensures that everyone uses the same terminology to describe the domain, which helps bridge the gap between the technical team and the business stakeholders.
In the code, terms from the ubiquitous language should be used to represent domain concepts, avoiding ambiguous terms.
For example, if your domain involves "Orders," "Customers," and "Payments," these terms should be consistently used throughout your code, avoiding synonyms like "Client" instead of "Customer."
Bounded Contexts
A bounded context defines the boundaries of a specific domain within a larger system. Each bounded context has its own model and ubiquitous language, and these contexts interact with each other through well-defined interfaces.
For example, in an e-commerce system, you might have separate bounded contexts for "Sales" and "Inventory."
Each bounded context will have its own representation of entities and value objects that are relevant to that domain. The term "Order" in the "Sales" context might have different attributes and behaviors compared to an "Order" in the "Inventory" context.
Benefits and challenges of DDD
Benefits
Challenges
Conclusion
Domain-Driven Design is a powerful methodology for tackling complex domains and aligning software with business requirements.
By building a rich domain model, adhering to ubiquitous language, and managing boundaries with bounded contexts, DDD can make software more maintainable and more aligned with business goals.
When working with DDD in C#, it’s important to utilize entities, value objects, aggregates, services, and repositories effectively. Although it can be challenging, the benefits of DDD make it a valuable approach for complex systems.
Développeur .NET | C#
3 个月Really insightfull, thank you !
Co-Founder & COO of Qlerify. Helping customers succeed with their software projects. | Techstars '23
3 个月Thanks for a great article! I am also passionate about bringing code closer to business needs. My colleague Staffan is going to talk about how to use AI to close the gap on Thursday. More info here: https://www.meetup.com/ddd-practitioners-in-hungary/events/301995144/?eventOrigin=group_upcoming_events
Lead Developer | Software Architect
4 个月How should you "split" the Order entity/aggregate into two distinct contexts (let's say Sales and Inventory)?
Engenheiro de Software Full-Stack e Especialista .NET | C# | React Native | Node.js | TypeScript | Angular | Go ?? Computer Engineer
4 个月Thanks for sharing, Balta!