Don't use Ids in your domain entities!
How often do you see code like this in your domain model?
public void Ship(int orderId, int customerId, string address)
{
Shipment shipment = _existingShipments.Single(x => x.OrderId == orderId);
if (shipment.CustomerId == customerId)
{
// Do something
}
}
Seems pretty good, doesn’t it? Well, it doesn’t. I’ve already pointed using Ids in domain entities as a bad practice, but I see developers - even sophisticated ones - write such code over and over again, so this topic definitely deserves a separate article.
Problem
One of the main DDD principles is separation of concerns. You should isolate your domain model from non-domain logic as fully as possible to avoid complexity overhead. That is especially true for domain entities because they are heart of your application.
Ids are essentially persistence logic implementation details; they have no relation to your domain. For example, you could use composite primary key instead of single-column key, but the meaning of the code would remain the same:
// Single-column key
if (shipment1.Id == shipment2.Id)
{
// The shipments match
}
// Multi-column key
if (shipment1.Id1 == shipment2.Id1 && shipment1.Id2 == shipment2.Id2)
{
// The shipments match
}
In addition to poor separation of concerns, the use of Ids breaks entities' encapsulation (aka Law of Demeter, aka Tell Don’t Ask):
// Seems nice
if (shipment1.Id == shipment2.Id)
{
}
// Still not bad
if (shipment1.Customer.Id == shipment2.Customer.Id)
{
}
// Well, maybe that's enough
if (shipment1.Customer.Address.Id == shipment2.Customer.Address.Id)
{
}
// Stop it!
if (shipment1.Customer.Address.City == shipment2.Customer.Address.City
&& shipment1.Customer.Address.Street == shipment2.Customer.Address.Street
&& shipment1.Customer.Address.State == shipment2.Customer.Address.State
&& shipment1.Customer.Address.Post == shipment2.Customer.Address.Post)
{
}
The last statement has an obvious design smell: it violates entities' encapsulation. The first (and the other two) statement has essentially the same drawback, the only difference between them is size of the code.
Solution
You don’t need Ids to operate your domain objects. In most cases, all you need is define equality members in the base entity class so that you could rewrite your code like this:
if (shipment1 == shipment2)
{
// The shipments match
}
Or:
if (shipment1.Equals(shipment2))
{
// The shipments match
}
Also, Ids in domain entities often indicate a hidden abstraction:
public void Deliver(
int fromAddressId,
int toAddressId,
int orderId,
int customerId)
{
if (_existingShipments.Any(x =>
x.FromAddressId == fromAddressId &&
x.ToAddressId == toAddressId &&
x.OrderId == orderId &&
x.CustomerId == customerId))
{
// Attach to existing shipment
}
else
{
// Create new one
}
}
In this example, Ids can be encapsulated in a separate entity:
public void Deliver(Shipment shipment)
{
if (_existingShipments.Contains(shipment))
{
// Attach to existing shipment
}
else
{
// Create new one
}
}
Such approach allows to keep complexity under control hiding all of the implementation details underneath of a separate entity.
I must point out an important note, though. All stated above refers to domain entities only. You can - and should - use Ids in infrastructure and application services, because Ids are natural for objects identification. Such application services operate on a different level of abstraction: they need Ids to map domain entities to database tables, to identify a web page requested by user and so on; they don’t contain any domain logic.
Summary
Ids in domain entities is a design smell. In most cases, they indicate poor entity encapsulation. If you want proper separation of concerns, you should reduce the number of Ids in your domain entities to as low as possible.
Heavy Ids usage is common for anemic model. There’s nothing wrong in it if you have a simple CRUD-like application. But if your application is large and complex, you should definitely choose rich model instead of anemic one, and thus, don’t use Ids in your domain classes.
Related articles:
Edit 2019-03-12:
A long overdue clarification:
Subscribe
Comments
comments powered by Disqus