29

In almost all circumstances, primary keys are not a part of your business domain. Sure, you may have some important user-facing objects with unique indices (UserName for users or OrderNumber for orders) but in most cases, there is no business need to overtly identify domain objects by a single value or set of values, to anyone but perhaps an administrative user. Even in those exceptional cases, especially if you are using global unique identifiers (GUID), you will like or want to employ an alternate key rather than expose the primary key itself.

So, if my understanding of domain-driven design is accurate, primary keys need not and thus should not be exposed, and good riddance. They're ugly and cramp my style. But if we choose not to include primary keys in the domain model, there are consequences:

  1. Naively, data transfer objects (DTO) that derive exclusively from combinations of domain models will not have primary keys
  2. Incoming DTO's will not have a primary key

So, is it safe to say that if you are really going to stay pure and eliminate primary keys in your domain model, you should be prepared to be able to handle every request in terms of unique indices on that primary key?

Put in another way, which of the following solutions is the correct approach to dealing with identifying particular objects after removing PK in domain models?

  1. Being able to identify the objects you need to deal with by other attributes
  2. Getting the primary key back in the DTO; ie, eliminating the PK when mapping from persistence to domain, then recombining the PK when mapping from domain to DTO?

EDIT: Let's make this concrete.

Say my domain model is VoIPProvider which includes fields like Name, Description, URL, as well as references like ProviderType, PhysicalAddress, and Transactions.

Now let's say I want to build a web service that will allow privileged users to manage VoIPProviders.

Perhaps a user-friendly ID is useless in this case; after all, VoIP providers are companies whose names tend to be distinct in the computer sense and even distinct enough in the human sense for business reasons. So it may be enough to say that a unique VoIPProvider is completely determined by (Name, URL). So now let's say I need a method PUT api/providers/voip so that privileged users can update VoIP providers. They send up a VoIPProviderDTO, which includes many but not all of the fields from the VoIPProvider, including some flattening potentially. However, I can't read their minds, and they still need to tell me which provider we are talking about.

It seems I have 2 (maybe 3) options:

  1. Include a primary key or alternate key in my domain model and send it to the DTO, and vice versa
  2. Identify the provider we care about via the unique index, like (Name, Url)
  3. Introduce some sort of intermediate object that can always map between persistence layer, domain, and DTO in a way that does not expose implementation details about the persistence layer - say by introducing an in-memory temporary identifier when going from domain to DTO and back,
10
  • 1
    Point to ponder: often times communication with domain experts gets impoverished when surrogate PK are used when a good business key exists. It seems we end up working for the ORM framework, instead of the other way around. Commented Aug 27, 2014 at 12:10
  • @user61852 well regardless of ORM, even if you really low-level, you still need a primary key in the implementation of the database layer. So I agree that surrogate PK gives you advantages over the actual PK used by a specific persistence mechanism, but if that PK is really representing a business object that is meaningful, it is necessarily unique and therefore has at least one unique business-related property defining it, no? Commented Aug 27, 2014 at 12:48
  • 1
    All of the advantages of surrogates are computer related and none human-related. Commented Aug 27, 2014 at 12:56
  • 2
    @user61852: I agree 100% (did I write something different?). For means of communication, use a "business key". Add unique contraints for any business key as well. But avoid the use of business keys to actually implement your database references.
    – Doc Brown
    Commented Aug 27, 2014 at 15:25
  • 2
    business keys are eternally unique - until they aren't. If you are using the business keys as primary when this happens then the change in business rules breaks more stuff.
    – psr
    Commented Aug 27, 2014 at 21:39

7 Answers 7

34

This is the way how we solve this (since more than 15 years, when even the term "domain driven design" was not invented):

  • when mapping the domain model to a database implementation or a class model in a specific programming language, you have a simple, consistent rule like "for each domain object mapped to a relational table, the primary key is "TablenameID".
  • this primary key is completely artificial, it has always the same type, and no business meaning - just a surrogate key
  • the "graphical version" of your domain model (the one you use to talk to your domain experts) does not contain primary keys. You don't expose them directly to the experts (but you expose them to anyone who is actually implementing code for the system).

So whenever you need a primary key for technical purposes (like mapping relations to a database), you have one available, but as long as you don't want to "see it", change your level of abstraction to the "domain experts model". And you don't have to maintain "two models" (one with PKs and one without); instead, maintain only a model without PKs and use a code generator to create the DDL for your DB, which adds the PK automatically according to the mapping rules.

Note that this does not forbid to add any "business keys" like an additional "OrderNumber", besides the surrogate OrderID. Technically these business keys become alternate keys when mapping to your database. Just avoid using these for creating references to other tables, always prefer using the surrogate keys if possible, this will make things a hell lot easier.

To your comment: using a surrogate key for identifying records is no business-related operation, it is a purely technical operation. To make this clear, look at your example: as long as you don't define additional unique-contraints, it would be possible to have two VoIPProvider objects with the same combination of (name,url), but different VoIPProviderIDs.

10
  • What about the step of taking the domain object from the returned persistence object (entity, or table row model, or whatever), going to DTO, and taking it back in, and going back to persistence? Is this just done via a surrogate key (ie a business-oriented definition of uniqueness) that requires a resolution on each persistence operation? Commented Aug 27, 2014 at 11:27
  • 1
    @tacos_tacos_tacos: let's stick to your VoIPProvider example. I would actually add a "VoIPProviderID" to your DTO, at least at the "implementation side" (if you have also a graphical version for your domain experts, I would probably not show it there). For updating purposes, the standard way of identifying a specific VoIPProvider should be by the "VoIPProviderID" you retrieved when pulling tha data from the database. If users of your API prefer identification by (name,URL), provide that additionally. ...
    – Doc Brown
    Commented Aug 27, 2014 at 11:56
  • ... and if performance seems to become a real, measurable problem, you can also consider to cache the mapping (name,url) to VoIPProviderID somewhere. But I would not recommend of implement such an optimization beforehand, prematurely.
    – Doc Brown
    Commented Aug 27, 2014 at 11:58
  • 1
    Natural keys sometimes seem really compelling but it's too easy to get burned (e.g., "whoops, now I have multiple tenants and that's no longer unique").
    – Casey
    Commented Aug 27, 2014 at 18:46
  • 2
    @corsiKa: in the context of what the OP asked, I would strongly recommend to have a purely autogenerated key "OrderID" (which is not printed on any receipt, but just used for internal things like database references), and a separate business key "OrderNumber" (which can, for example, contain something like the current year, which can be used for sorting and filtering, which can be changed/corrected afterwards, and which can be printed on receipts). OP asked for "Domain Driven Design", the "OrderNumber" is part of the domain model, whilst the "OrderID" is just an implementation detail.
    – Doc Brown
    Commented Aug 28, 2014 at 6:12
4

You need to be able to identify many objects by some unique index, and isn't that what a primary key is (or at least implies one is present).

Unique indexes are available so you can further constrain your DB schema, not as a wholesale replacement for PKs. If you're not exposing PKs becuase they;re ugly, but are exposing an unique key instead... you're not actually doing anything different. (I assume you're not getting a PK and an identity column mixed up here?)

8
  • When I want to do an operation involving a specific instance of a domain object that is persisted, I need to be able to include in the DTO either explicitly (via some sort of key, either primary or an alternate user-friendly ID that is unique to that table and might as well be an index on the table) or implicitly (via some combination of fields whose values uniquely identify a specific record)... and furthermore, in a practical sense, I would need to send up in the DTO some form of change tracking if I were to do it the other way (OriginalVal vs NewVal for all fields that identify record),no? Commented Aug 27, 2014 at 11:10
  • isn't the explicit v implicit question the same difference? You can have a PK that spans multiple columns, just like a unique index. I don't see there is any difference between them for your purposes.
    – gbjbaanb
    Commented Aug 27, 2014 at 12:04
  • Sure, we could have a PK on multiple columns for instance. But to me it is leaking something about the databaswe (the storage) which should have NOTHING to do with the heart & soul, the business entity. If some tuple of business entity fields happens to span the PK for the DB, then great. But it shouldn't necessarily be the other way around, should it? Commented Aug 27, 2014 at 12:39
  • You're overthinking it. A unique index is just as much an artifact of the DB schema as a PK is. Think of it this way - a PK is just the first (or primary) unique index. its 'special' because you only generally need 1 such index.
    – gbjbaanb
    Commented Aug 27, 2014 at 12:43
  • True, but any meaningful domain object should be identifiable strictly from at least one of its business-related fields, no? The fact that this defines an index in the DB is more for performance reasons than to be used to query the DB easily... I would prefer a one-column PK over a 6-column unique index, and they serve different purposes really - a PK (or index with small number of fields) is also available for the DBA/DBD's convenience, right? Commented Aug 27, 2014 at 12:51
4

Without primary keys in the frontend there is no easy way for the backend to know what you are sending. To fix that you would need a ton of extra work parsing the data, which would hurt performance and probably take more time and be more ugly than attaching a key to every item.

As an example, lets say that I want to edit a message in an app; how would the app know which message I want to edit without a primary key attached? Editing objects happens all the time and doing that without keys is nigh impossible. But if you have objects that aren't supposed to be edited then skip the key if you think that it is distracting, but having primary keys can improve performance here.

3
  • 1
    Well, the message is an extreme example, but even then we would know MessageSender, MessageRecipient, TimeSent - that should be unique. Commented Aug 27, 2014 at 11:28
  • 1
    @tacos_tacos_tacos, then how do you create FKs to the other tables? It should be MessageSenderId, which probably maps to a Users table on UserId. You wouldn't want to use the UserName as the key between tables, as that could change and becomes a maintenance nightmare. This is why you generally only join tables using Primary Keys, and not another column (there are certainly exceptions). This db structure still must be enforced. Now you could always go to a CQRS model for your app...in which case the rules change. Especially if you also use Event Sourcing.
    – CaffGeek
    Commented Aug 27, 2014 at 21:13
  • I don't fully agree with this answer. I believe that all entity identifiers should be generated by the (backend) application itself. In some cases, like the message, the entity's obvious attributes are not enough to distinguish it efficiently. It may be a business decision to tag messages with identifiers. These identifiers should be generated by the application. These identifiers are not necessarily primary keys. Primary keys can be generated by the db but should be kept there. Database generated primary keys are not domain concepts!
    – alaboudi
    Commented Mar 6, 2021 at 16:42
4

The reason we use a non-business related PK is to ensure that our system has an easy and consistent method of determining what the user wants.

I see you replied with a comment: MessageSender, MessageRecipient, TimeSent (for a message). You can STILL have ambiguity this way (for example, with system generated messages triggering on something that happens often). And how are you going to validate MessageSender and MessageRecipient here? Suppose you validate them using FirstName, Lastname, DateOfBirth, you're going to eventually run into a situation where you have 2 people born on the same day with the exact same name. Not to mention that you will run into a situation where you have a message named tacostacostacos-America-1980-Doc Brown-France-1965-23/5/2014-11:43:54.003UTC+200. That's a monster of a name, and you still have no guarantee you'll only have 1 of those.

the reason we use a primary key is because we KNOW it'll be unique for the lifetime of the software, no matter what data is entered, and we KNOW it's going to be a predictable format (what happens if your above key has a dash in a username? your whole system goes to shit).

You don't need to show your ID to your user. You can hide that (in plain sight if necessary, through the URL).

Another reason why a PK is so useful is something you can deduce from the above: a PK makes it so that you don't have to force the computer to interpret user-generated code. If a Chinese user uses your code and enters a bunch of Chinese characters, your code suddenly doesn't need to be able to work with these internally, but it can simply use the Guid that the system generated. If you have an Arabic user who enters Arabic writing, your system doesn't have to cope with that internally, but can basically ignore that they're there.

As others have said, a Guid is something that can be stored internally in a fixed size. You know what you work with and it's something that can be universally used. You don't need to create design rules on how you create a certain identifier and save it. If your system only takes the first 10 letters of a name, it doesn't see any difference between Michael Guggenheimer and Michael Gugstein, and it will confuse these 2. If you cut it off at any arbitrary length, you can run into confusion. If you limit user input, you can run into issues with user limitations.

When I look at existing systems like Dynamics CRM, they also use the internal key (the PK) for the user to call upon a single record. If a user has a query that doesn't involve the ID, they return an array of possible answers and let the user choose from it. If there is any chance at ambiguity, they'll give the choice to the user.

Finally, it's also abit of security through obscurity. If you don't know the record ID, your only option is to guess it. if the ID is easy to guess (because the information that it's made of is publicly available), anyone can change it. You can even goad the user into changing it through classic CSRF or XSS methods. Now, obviously your security should already have those accounted for and mitigated before ever publishing the live version, but you should still make it harder for potential abuse to happen.

1

When issuing an identifier for an external system, you should only give URIs, or alternatively a key or a set of keys that have the same properties as a URI, rather than exposing the database primary key directly (from here onwards I'll refer to both URI or a key or a set of keys that have the same properties as URI as just URI, in other word a URI below doesn't necessarily mean RFC 3986 URI).

A URI may or may not contain the primary key of the object and it may or may not actually be composed of alternate keys. It doesn't really matter. What matters is that only the system that generates the URI is allowed to split or combine the URI to form an understanding of what the referred object is. External systems should always use the URI as an opaque identifier. It doesn't matter if a human user can identify that one part of the URI is actually a database surrogate key or that it consists of several business keys bunched up together, or that it's actually a base64 of those values. These are irrelevant. What matters is that the external system shouldn't need to be required to understand what the identifier means to use the identifier. External systems should never be required to parse the components within the identifier or combine an identifier with other identifiers to refer to something in your system.

Using GUID fulfills some of this criteria, however identifier like GUID can be difficult to dereference back into the object even within your system, so keys that are opaque to even your system like a GUID should only be employed if client parsing the URI/identifier actually poses a security risk.

Back to your VoIP example, say that a VoIP provider can be uniquely determined either by (VoIPProviderID) or by (Name, URL) or by (GUID). When an external system needs to update the VoIP provider, it can just pass a PUT /provider/by-id/1234 or PUT /provider/foo-voip/bar-domain.com or PUT /3F2504E0-4F89-41D3-9A0C-0305E82C3301 and your system would understand that the external system wants to update the VoIPProvider. These URIs are generated by your system and only your system need to understand that all they means the same thing. The external system should just treat whatever is in the URI as basically a PUT <whatever>.

Suppose that you have the data for different VoIP providers stored in different tables with different schemas (thus completely different set of keys identifies each VoIP provider based on which table they are stored in). When you have a URI, they can be accessed uniformly by the external system, without regards of how your system identify the particular VoIP provider. To the external system it's all just opaque pointer.

When your system uses a URI to refer to objects in such a way, you're not leaking anything regarding how you implement your system. You generate the URI and the client simply passes it back to you.

0

I am going to have to target this wildly inaccurate and naive statement:

Perhaps a user-friendly ID is useless in this case; after all, VoIP providers are companies whose names tend to be distinct in the computer sense and even distinct enough in the human sense for business reasons.

Names are terrible as keys, as they are frequently changing. A company may have many names during its lifetime, the company may merge, split, merge again, create a distinct subsidiary for specific tax purposes that has 0 employees but all customers which then hires employees from a completely different subsidiary.

Then we get into the fact that company names are not even remotely unique, as shown by the landmark Apple vs. Apple.

A good object relational mapper or framework should abstract the primary keys away and make these invisible, but they are there, and they will usually be the only way to uniquely identify an object in your database.

For reference, I prefer how django handles this:

class VoipProvider(models.Model):
    name=fields.TextField()
    address=fields.TextField()

class Customer(models.Model):
    name=fields.TextField()
    address=fields.TextField()
    voipProvider=fields.ForeignKeyField(VoipProvider)

In this way, the details of a provider of a customer can be access in code by using:

myCustomer.voipProvider.name #returns the name of the customers VOIP Provider.

While the Primary/Foreign keys aren't visible, they are there, and can be used to access items, but are abstracted away.

1
  • You're absolutely right, but I guess I meant that "in some domains, maybe there is a natural tuple of values that are always unique" If they change, but they're still unique, they still identify a single record. Commented Aug 28, 2014 at 7:15
0

I think we often still incorrectly look at this issue from the perspective of the DB: oh, there's no natural key, so we need to create a surrogate key. Oh no, we can't expose the surrogate key back into the domain objects, that's leaky etc.

However, sometimes a better attitude is this: if a business (domain) object doesn't have a natural key, then perhaps it should be given one. This is a two-fold business domain issue: Firstly, things need an identity, even in the absence of databases. Secondly, although we try to pretend persistence is some abstract idea invisible to the domain, the reality is that persistence is still a business concept. Obviously, there are issues where the chosen natural key isn't supported by the DB as a primary key (e.g. GUIDs on some systems) - in this case you will need to add a surrogate key.

Thus, you end up in a very similar place, e.g. your customer has an integer ID, but instead of feeling ill because that has leaked from the DB to the domain, you feel happy because the business has agreed that all customers shall be assigned an ID, and you are persisting that to the DB, as you must. You are still free to introduce a surrogate, e.g. to support renaming the customer ID.

This approach also means that if a domain object hits the persistence layer, and doesn't have an ID, then it's probably some sort of value object, so doesn't need an ID.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.