What Should I Use As An Id For My Domain Entities?
Publié le 20/02/2025
Par  Christophe MOMMER

When designing a system with an advanced architecture, it is necessary to assign an identifier to its domain entities. Note that I’m referring here to domain entities in the DDD (Domain Driven Design) sense, not to business entities that might be used for database storage. 

Every entity/aggregate must be uniquely identifiable with an identifier. In some cases, the type of identifier is imposed by the business. For example, in a healthcare management system, it makes sense for a patient’s identifier to be their social security number, so a string type is perfectly appropriate here.

But very often, the business imposes values rather than identifiers, and since uniqueness isn’t guaranteed, it’s up to the developer to handle this. This brings us back to the same question: which type of identifier should be chosen?

We naturally draw on what we already know, so the debate between int vs. Guid comes into play. Let’s quickly analyze the pros and cons of each approach.

In the case of an int, it goes without saying that it aligns well with human logic: counting elements. It’s natural and obvious that after entity 1 comes entity 2. When you analyze the relationships between elements in your system — say, during a debug session — it’s easy to understand and remember that you’re dealing with entity N. Integers therefore appear to be a natural key type: easy to read, understand, and recall.

Nevertheless, they are not without drawbacks. For one, they’re predictable. So if you decide to expose your entity in some way (for example, via an API), you probably shouldn’t reveal the raw integer system value. Doing so might open a potential security vulnerability for ill-intentioned individuals who could then predict the existence of certain elements, even if they’re not supposed to have access to them. Of course, you can secure this, but at the cost of extra effort that you might avoid simply by hiding the value.

For me, however, the main drawback isn’t predictability: it’s the necessity to maintain a sequence to avoid duplicates and to properly manage “gaps” when an entity is deleted. This requires a service to track the counter value to ensure every entity in the system has a unique identifier, along with all the challenges that entails (I’m thinking in particular of multi-threading to guarantee uniqueness). Of course, reputable databases offer auto-incrementing ints for your rows, but remember that some domain entities aren’t necessarily tied to a specific table, so that’s not always a valid solution. Moreover, in some systems, it might make sense to delegate the responsibility of creating the identifier value to the client, which further complicates an integer-based approach.

Therefore, the alternative is to use a pseudo-random value whose generation isn’t based on a logical and predictable system: the Guid.

I think everyone immediately sees the drawback of a Guid: it’s long and defies human logic. It’s always difficult to remember an entire Guid during a debugging session and you probably end up, like me, only remembering the first and last few characters.

However, a Guid has the advantage of being generated without constraints and, at least in .NET, can be used in high-load scenarios, with the risk of collision being extremely low.

But here’s the catch: it’s not without flaws. Beyond its unreadable nature, Guid v4 has a major drawback—its generation is completely random, meaning two elements created just a few milliseconds apart will have diametrically opposed values. This is why Guid v7 aims to fix that by incorporating temporal information in the first part of the value. And since .NET 9, it’s easy to generate them! 

Guid id = Guid.CreateVersion7();

This solves the issue of entity ordering when sorted by their id and, although it is more resource-intensive in the database than an int, the ordering helps us avoid potential performance issues (see the blog mentioned at the beginning of the post).

But… it’s not perfect. Simply because v7 is more resource-hungry and takes longer to generate than v4 (in .NET 9, this is significant—perhaps performance will improve in future versions).

Moreover, even though v7 is a considerable improvement, there is still a risk of fragmentation if the system is under heavy load.

Which brings us to our “ultimate” type: the ULID (Universally Unique Lexicographically Sortable Identifier).

The ULID specification aims to “fix” the shortcomings of its big brother, Guid v7, notably through its design for high-load environments, its efficient encoding, and the use of Crockford’s Base32 to avoid ambiguous characters.

Now, one question remains: how do you generate them in .NET?

For now, only the community approach offers a solution with the Ulid package on NuGet.

Its usage is remarkably similar to that of the Guid class and offers top-notch performance (here’s the benchmark result for generating a new value with .NET 9):

Of course, this type of identifier can also be generated by the client and allows us to maintain a sequence. The elimination of ambiguous characters also makes it somewhat easier to read compared to other random identifier types. For me, this makes it an excellent candidate for the identifiers of your domain objects! (Although, of course, each system has its own constraints and this will need to be evaluated according to your specific situation.)