Feedback on Domain-Driven Design
Domain-driven design (DDD) is a method centered around the business domain. It aims to enforce one simple principle: “Every business concept must be represented in the domain (code),” or at least that’s what I thought when I first became interested in it. In this post, I’d like to share what I’ve learned about DDD by exploring its key concepts: ubiquitous language, bounded contexts, aggregates, aggregate roots, entities, and value objects.
Definition of Ubiquitous Language
To effectively understand the business domain problem, developers and the business domain must share a common language. This reduces ambiguity in understanding the problem or the needs of the business domain. This is the ubiquitous language. Let’s look at how this works in a simplified version of an e-commerce site:
Above, three objects represent:
- A Product sold by the site
- A ShoppingCartItem that shows how many of a certain item are in the shopping cart
- The ShoppingCart, which reflects the intent to purchase and lists all the products
At this stage, we have represented business domain concepts in the code to identify their nature and start to understand the behavior.
Ubiquitous Language: A Crucial Step
This is a crucial step. If neglected, it can be frustrating for both the business domain, which may think it has been understood, and the developers, who may have made unnecessary assumptions about certain limitations.
It’s common to go back to concepts that seemed clear initially but have been refined over time. Dialog between the business domain and the developers is key.
By using a clear, ubiquitous language, we can identify the most relevant contexts for each business domain concept.
Definition of Bounded Contexts
Bounded contexts may be present if, during a project’s life cycle, certain business domain concepts make little or no sense in a particular context but are critical to another. The idea is to group coherent concepts together when solving a problem. Returning to our e-commerce site example, we can identify three contexts from the concepts we have seen so far.
Let’s take a look at how the bounded contexts are broken down:
- The Product is in the Product Catalog context
- The name of the ShoppingCartItem is shortened and incorporated into the Shopping Cart context
- The cart is then turned into an order and added to the Order management context
Each dedicated context is a solution to a problem. This solution can be complete or partial. The problem to be solved is called the subdomain and corresponds to a part of the project problem.
|E-commerce site||Product catalog management||ProductCatalog|
Here, the dedicated domains ProductCatalog, ShoppingCart, and ShipmentManagement are all linked to a single subdomain, while OrderManagement spans two subdomains. But why? Because the business domain wants to be able to deliver an order to more than one address, depending on the product (for example, if there is a product exchange or if there are different delivery times based on the recipient’s location).
NOTE: Keeping a 1:1 relationship between the subdomain and the related dedicated context is strongly advised to keep the project intelligible.
Note also that the ShoppingCart concept represents a dedicated context for effectively solving cart management issues (calculating amounts and adding or deleting items from the cart).
Key Takeaways for Dedicated Contexts
Dedicated contexts help you understand the business domain better by highlighting the most important issues. This makes it easier to deal with the problem and limit its effects.
Entities and Value Objects
An entity is a concept within your business domain with a clear identity and life cycle. Its identity never changes during its life cycle, but its attributes may. Its main job is to define what is meant by the phrase “Entity A and Entity B are equal if…” This is typically done by overriding the Equals method.
The code above defines an entity’s attributes and how to distinguish between entities in an abstract class.
In our domain, the product has a life cycle demonstrated by the fact that its price or name can change. For this reason, it seems a good candidate for an entity since entities are mutable objects.
It’s worth noting that delegating responsibility for equality to the abstract Entity class simplifies the definition of the Product class by removing noise about its identity and equality.
A value object is a business domain concept defined only by its characteristics or state. It is used to describe a concept or business domain element. It has no identity within the domain because its attributes are its identity. It is always used within a more complex object and, therefore, understood within a specific context.
Above, we have a proposed value object: the equality override takes into account all fields returned by the GetAttributesToIncludeInEqualityCheck method by comparing the attributes of each value object. The object’s hash is also affected by the object’s attributes.
The value object is immutable, so when it is loaded, a new instance is created in a state that includes the changes. Money is a good example of a value object.
In this proposal for Money, we can see that both the Value and Currency fields are used to determine the equality of Money. We can also see that methods that involve a change of state create a new instance of Money. Also, the concept of Currency is given to another value object.
Entity and Value Object: Key Takeaways
Ultimately, identifying entities and value objects is fairly easy. The main way to tell them apart is by whether or not this element/concept can only be identified by its attributes. If so, it’s a value object. If not, it’s an entity. Value objects contain a large proportion of the business concept rules. Establishing relationships between entities and value objects helps to identify domain activities.
Aggregates and Aggregate Roots
Entities and value objects work together in the domain to form complex rule relationships. When dealing with complex domain objects, ensuring their consistency and concurrency can be challenging. The example below shows a representation of order management. If we considered all of these objects as a single business domain concept, it would be extremely complex to maintain and would also have an impact on the application’s performance.
The problem with the representation in this example is that the customer account address can be changed while an order is in progress. This raises the issue of whether the change should be implemented immediately or after the order in progress has been delivered.
An aggregate is a cluster of associated objects that we treat as a unit for the purpose of data changes. Each aggregate has a root and a boundary. The boundary defines what is inside the aggregate. The root is a single, specific entity contained in the aggregate. The root is the only member of the aggregate that outside objects are allowed to hold references to.
Eric Evans – Author of “Domain-Driven Design: Tackling Complexity in the Heart of Software”
Let’s use our previous example to try and understand this definition. We can identify three clusters:
- Customer, responsible for managing customer accounts
- Order, responsible for managing orders
- Product, responsible for managing the product catalog
Each cluster can be treated as a single, consistent unit with respect to its invariants, entities, and value objects.
Once this tagging work has been done, it is quite easy to identify the root of each aggregate. In our case, these are Customer, Order, and Product.
Relationships between the aggregates are then represented by referencing the aggregate root IDs rather than the objects themselves. This is to maintain the boundaries of each aggregate and limit the size of the object graph to be loaded.
An aggregate’s invariants are the rules that ensure the aggregate’s business domain consistency. They must always be checked throughout the aggregate’s life cycle. They are the management rules applied to the aggregate.
Let’s look at how they can be defined in the code:
OrderItem is a value object from the Order aggregate. The order ensures that the number of items does not exceed the maximum limit. Note that order items cannot be changed outside the aggregate but can be viewed via the Items property.
What about adding a product with a negative quantity? The invariants of the OrderItem value object control this, as seen in the code below:
Helpful Information About Aggregates
Aggregates are key concepts within the domain, so changing them requires particular care. You need to know the business domain to identify them. Here are some pointers we’ve learned from experience:
- Consider using diagrams to understand the interactions between entities and value objects to help you identify your aggregates.
- The aggregate root is a single aggregate entity, but its context may have more than one aggregate.
- The aggregate root ensures the aggregate’s consistency and concurrency. Its methods, which contain and apply its invariants, are used to make changes.
- If you start with small aggregates, transitioning to larger ones will be easier.
- The aggregate is read and persisted in its entirety.
Creating Complex Objects
The constructor is the preferred way to create an object (entity or value object). However, we recommend using the Factory pattern to consolidate all the management rules in a single location for complex objects.
Here, the Order object is used to create an order with items, whereas in the previous implementation, items had to be added to the order at a later stage.
Persistence and Hydration
The Repository pattern can be used to retrieve and hydrate aggregates because it abstracts the persistence of model aggregates. It is essential to remember that aggregate persistence is never partial. Even if the change is only partial, the aggregate must be considered a unit that cannot be separated from its parts. This means that if an aggregate changes, it goes through the following steps:
This process can be complicated, so it is only recommended when an aggregate needs to be created and/or changed. In read-only instances, it is better and less costly to query the database directly using solutions that are more optimized for reading (e.g., views or stored procedures in SQL).
More Information About Domain-Driven Design
We’ve looked at some of the key concepts behind domain-driven design (DDD) and how we can use them in our code.
Dedicated contexts help us identify important behavior patterns in our business domain. These behaviors are represented by connecting basic building blocks: entities, which are objects with an identity and a life cycle, and value objects, which can be identified by all their attributes and are immutable.
These basic building blocks are grouped together in clusters called aggregates. A single entity is defined as the aggregate root. Aggregates are treated as single units. If you want to change one of their components, this must be done via the aggregate root.
All of these concepts are part of DDD’s tactical patterns for setting up your code base. There are also strategic patterns that aim to identify how bounded contexts in a distributed system often communicate with one another. A huge topic that we’ll look at in more detail in a future post.