Unique identity is the main characteristic of Entity in DDD. It manifests itself through the object Id, which, in real world, is often… an int from the database. If DDD is all about business modeling this is probably not the best choice. In this article, I will try to present the alternative approach and its implementation which allows us to introduce business identifiers to our Entities in a pretty effortless way.

The concept and suggested solution are applicable if the system is based on DDD principles and hexagonal architecture (or its equivalents).

Problem

Entity Id should have business meaning

The domain objects’ Id should mirror the way the business distinguishes objects and how it perceives their identity. And what’s the reality?

We don’t have such a thing, we use integers generated by the database.

You don’t, do you? What does it really mean that we distinguish objects solely by their number from a given persistence mechanism? Either it is not a real domain object, but rather an anemic Entity from CRUD part of the system; or no one has ever considered what the business identifiers are (because the integers from the database have always been there at hand) .

Business and technical Id are two different things

So what? Am I supposed to use varchar or even something worse as a primary key?

Absolutely not! The database has its own set of rules and if it needs its technical keys, it should have them. It’s all about the fact that the technical concepts should not leak into the domain. The DDD Entity is a domain term and as such it should be as little dependent on the technical concepts as possible. In particular, this applies to dependencies which require additional fields, or an act of making some part of the state public, or introducing purely technical inheritance.

So how am I supposed to distinguish if the object is new or was previously saved? Am I to use the ORM? That’s even worse…

Is persistence a domain term? Should business logic depend on that? The fact that an object is new or not, or which version it is, is relevant, but not in the domain logic. None of the domain experts has a slightest idea what are: an int, a transaction or optimistic locking. If the business starts using that sort of language, that doesn’t sound well.

Okay, but in repository, I really need to know if the object is new or not, which version it is, etc. Now what?

True, that sort of knowledge is required at that stage, but here inheritance comes to the rescue. In the domain we shouldn’t be interested in technical concepts; but nothing stands in the way for the object representing the domain Id to be something more than the domain knows about. It is the repository that restores a given object and gets it back for a new save that knows about it.

Solution

Technical overhead should be minimal

The introduction of business Id and derived from it technical Id is a sort of an overhead. Just like every mechanism, this one also requires some additional code, which, after all, we’re trying to reduce. What’s more, developers are allergic to trivial code repeating itself in an almost identical form. The solution should thus minimize that overhead so that cost of its introduction stays acceptable.

Every element should have single natural responsibility

My proposition is based on the following elements:

Entity – base class responsible for identification through business Id

DomainId – business identifier for every Entity; stores business Id

TechnicalId – technical identifier storing technical Id as well as information if object is new, and possible version

What’s essential, DomainId cannot be simply business Id. If that was the case, TechnicalId derived from it for a particular Entity wouldn’t be possible. DomainId stores the business Id value and correctly implements methods responsible for object equality.

Entity stores DomainId and delegates there logic responsible for object equality.

TechnicalId of a particular class is derived from its DomainId, and thanks to that it can be used everywhere where DomainId is expected.

The number of types that are to be added to the project should be minimal

Implementing DomainId for every class would be a repetitive task. Thus it should be avoided so that deriving from Entity should be enough. No additional line of code should be needed. Unfortunately, implementing separate TechnicalIds for every Entity might be unavoidable. This overhead can be reasonably minimized, or even completely eliminated when applying appropriate mechanisms in persistence. But that’s a topic for another day.

Implementation

Sample implementation will be in C#. The way the C# generic types work allows us to acheive our goal in a neat and simple manner. It is possible to devise similar implementation in other languages.

Declaration of a particular Entity type could look like this:

public class Order : Entity<Order, OrderNumber>   

DomianId is a nested class in Entity. Thanks to generic self-referencing declarations the DomainId of every Entity is a different type. Simultaneously, the constraint on the argument is strong enough, so that incorrect use is practically impossible. The base class itself is very simple and responsible only for identification through DomainId.

Here is an example:

Order.DomainId domainId = new Order.DomainId(new OrderNumber("ABC/123"));

Order order = Order.Create(domainId);

Order.DomainId domainId = order.Id;

DomainId can be cast implicitly to business Id, hence no additional operators are necessary:

 OrderNumber orderNumber = domainId; 

TechnicalId declaration could look like this:

 public class OrderTechnicalId : Order.DomainId, ITechnicalId<int> 

ITechnicalId<TValue> interface provides two properties essential for persistence: TechnicalValue and IsNew. The implementation of particular types is radically simplified thanks to TechnicalId<TValue> struct and delegation of ITechnicalId<TValue> interface implementation to it.

TechnicalId can be passed as DomainId on Entity creation:

 Order order = new Order(new OrderTechnicalId(new OrderNumber("ABC/123"), 123456)); 

Determining whether the object was created in the domain or recreated in persistence mechanism takes place through checking if its Id implements ITechnicalId. If a new, previously unsaved object was created outside the domain, this can be checked through IsNew property.

Feel free to visit our GitHub, where you can find complete code for this solution and many more. You can also download readymade NuGet packages.

Summary

Business Id increases model explicability and eliminates mistakes

Another advantage of this solution is decreased susceptibility to mistakes. Having separate Ids for every type in the domain protects us against silly mistakes, eg. if somebody passed client Id instead of product Id. When everything is an int, such mistakes are easy to make.

Thanks to business Ids clarity of code increases, because all types pronounce themselves. We very often use Ids instead of whole objects in the domain. For example – aggregates do not store references to other aggregates, but their identifiers. So, to make code more safe and understandable we shall use compiler to make sure that certain type of identifier can be used only in the places where it’s expected.

When to use?

Is it always necessary to use such sophisticated approach? Of course not! It’s useful only when it leads to a better model. If it’s artificial, it shouldn’t be done. You should always ask yourself if the overhead is artificial or the problem trivial? Or even if it holds true now, will it still be true in a month’s or year’s time? What is the system’s lifespan? How many people will work on it? In case of the approach described in the article return on investment is very quick and it is often worth putting in this additional work.

What’s next?

Does it mean a total separation of the domain model from persistence? Unfortunately not. The rest of Entity data is missing and it has to be saved too, one way or another. Next time, we’ll be dealing with the techniques used to achieve this goal and the associated costs.

Categories: DDD

Leave a Reply

Your email address will not be published. Required fields are marked *