The Role of a Unique Identity in Entities
Do you really have a domain model, or is it just a data model?
Database-generated integer IDs are ubiquitous in tutorials, auto-generated code (which I find creepy in and of itself), and numerous software projects. Despite technological and methodological advances, this practice is persistently part of the developer's toolkit, often without much thought to its implications. In this article, we will explore the significant impact this "small" detail can have on our modeling approach, and why it's time to rethink the way we think about and implement identifiers in our systems.
It is somewhat understandable that integer IDs appear in certain scenarios. Consider a small, straightforward application whose single purpose is to input and display data via a user interface. In these cases, where complexity is minimal, no business logic is involved, only basic CRUD operations are performed, and nothing is distributed, the use of integer IDs may not attract attention. Especially when no business logic is being enforced behind the scenes, this approach may be sufficient.
But to be realistic, after 30 years in the software industry, complexity is usually underestimated. Domain experts often think it's simple, and after a few weeks they realize how complex it really is. Or later, new complex requirements are added that require a domain model.
However, if we venture into the area of complex process modeling, messaging, or distributed applications. In these contexts, relying on database-generated integer IDs is a bad practice and a fundamental misstep. And why is that?
Integer IDs belong to databases, not to the domain itself!
It's a common assertion among developers to claim their systems are built around domain models. However, a critical review often uncovers a reliance on auto-incremental integer IDs, sourced directly from databases, which is a clear deviation from the concept of a persistence-independent domain model.
When we use database-generated integer IDs in the domain model, it shows we're mixing up concerns that should be kept apart. This mix-up goes against the Separation of Concerns (SoC) principle, revealing that what we have isn't truly a domain model but rather just a data model. This principle tells us to keep different parts of the system — like the domain logic and how data is stored — separate. This way, each part can do its job without getting tangled up with others.
The following example shows a class that just represents a database table. This is not a domain model. (Even though people have often tried to sell it to me as an domain model.)
[Table("Products")]
public class Product
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
[Required]
[StringLength(100)]
public string Name { get; set; }
[Column(TypeName = "decimal(18,2)")]
public decimal Price { get; set; }
[StringLength(255)]
public string Description { get; set; }
}
Even if behavior were added to this class, it would remain closely linked to the database. This would mean that the object would first have to be persisted in order to obtain an identity.
Entities and Identity in Domain-Driven Design
DDD uses a set of patterns for effective tactical design, including entities, aggregates, value objects, factories, and repositories. Entities stand out as they form the backbone of our domain, representing core business objects within a bounded context. Entities in DDD are distinguished by their constant identity, behavior and their own lifecycle. They are about domain behavior.
Entities bring depth and meaning to the domain because they are uniquely identified by their identity. Their identity is what distinguishes them from each other, even if other attributes are the same. Consider two people with identical names, ages, and jobs. It's their unique identities - not just their names or job titles - that differentiate them.
Value objects, on the other hand, have no identity and no life cycle. They represent themselves only by their values. They are always immutable.
But how do we assign these identities? Given our goal of avoiding tight coupling and enforcing SoC, it's clear that retrieving identities from the database is not the right approach. Instead, the entity itself should take care of generating identifiers, often during the creation of a new object via a factory method. A common strategy is to use a Global Unique Identifier (GUID) or Universal Unique Identifier (UUID) to ensure that each entity has a unique, domain-generated identity.
Let's look at a simple example of what an entity might look like without having to depend on a database.
public class Product
{
public Guid Id { get; private set; }
public string Name { get; private set; }
public decimal Price { get; private set; }
public string Description { get; private set; }
// Private constructor used for creating new instances with a factory method
private Product(string name, decimal price, string description)
{
Id = Guid.NewGuid(); // Assign a new unique identifier
Name = name;
Price = price;
Description = description;
}
// Public constructor for reconstituting an existing instance from storage
public Product(Guid id, string name, decimal price, string description)
{
Id = id;
Name = name;
Price = price;
Description = description;
}
// Factory method for creating new Product instances
public static Product CreateNew(string name, decimal price, string description)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Product name cannot be empty.", nameof(name));
if (price <= 0)
throw new ArgumentException("Price must be greater than zero.", nameof(price));
return new Product(name, price, description);
}
// Additional domain logic and methods here
}
Specific Purpose and Meaning
Introducing a special way of handling IDs in our applications increases the clarity and security of our domain models. To make it even more perfect, we encapsulate our very general IDs in a value object like ProductId, giving them a specific purpose and meaning that goes far beyond that of a mere identifier. This is where the ubiquitous language comes in. It gives each ID a role and ensures that it is recognized not just as any Guid, but as an integral part of the domain to which it belongs.
This approach underscores the importance of treating IDs as a domain concept rather than just a database requirement. It's about reinforcing the idea that every element within our domain model, including IDs, should have a defined role and purpose that aligns with our understanding of the domain itself.
Consider this C# example where we define a ProductId as a Value Object:
public readonly record struct ProductId(Guid Value)
{
public static implicit operator Guid(ProductId productId) => productId.Value;
public static implicit operator ProductId(Guid value) => new ProductId(value);
}
This encapsulation increases the expressiveness of our model and also strengthens the integrity of the domain by preventing misuse of IDs.
Closing Thoughts
Using unique identifiers such as GUIDs or UUIDs instead of traditional integer IDs better aligns with DDD principles. They also greatly enhance your system's ability to scale and integrate across distributed environments. Such identifiers ensure the uniqueness of each entity across contexts and systems, eliminating the risks associated with duplicate or conflicting IDs. By making this shift, developers can build models that are not only better aligned with the realities of their domain, but also more versatile and future-proof in the increasingly networked landscape of modern software development. Adopting this approach is a critical step toward creating systems that are truly robust, scalable, and distributable.
Cheers!