11

In a hypothetical system that handles adding users, there are several business rules. Some of the rules can easily be checked in the model. For example a user registration can only be saved if they entered a 10 digit phone number.

But what if we want this phone number to be unique?

In most databases it's fairly easy to add a constraint that generates an error when trying to store a duplicate value. But when following that approach, the model doesn't explicitly make clear that a phone number should be unique. If the model is reused or the database is changed, these business rules could be overlooked.

If we want to encapsulate this knowledge in the domain, we could create a Domain Service (since Entities are not supposed to communicate with the outside world like the database). This UserService could use the UserRepository to check if the phone number already exists.

The second approach requires more code and an additional round trip to the database, but it improves the domain model.

Are there (better) alternatives? Which approach would you choose and why?

8
  • Something that has bugged me for a long time is the duplication of business rules across layers. For the example of a business rule like "Phone must be unique," it feels wrong to be to have a database persistence rule in place (a SQL UNIQUE constraint) AND some business layer code that checks uniqueness as well.
    – GHP
    Commented Feb 5, 2019 at 14:10
  • My unease comes from the fact that if you bring in someone new to the system and say "Please remove the business rule about phones being unique" then they are likely to spot the first place they find with such a rule, and assume that that's the only place the rule lives. I know that in this trivial case, any degree of testing would reveal that the code must be changed in two spots, but why are we designing our systems to be harder in this way? Why is the Don't Repeat Yourself rule excluded when designing business rules?
    – GHP
    Commented Feb 5, 2019 at 14:10
  • 1
  • 1
    @Graham There are always database persistence rules in place. Data types are such a rule. Commented Feb 5, 2019 at 15:08
  • 1
    Also "Duplication is far cheaper than the wrong abstraction" is a very powerful rule of thumb: sandimetz.com/blog/2016/1/20/the-wrong-abstraction Commented Feb 5, 2019 at 15:08

8 Answers 8

5

Ask yourself, is there any piece of domain logic in your User that actually cares about whether or not it's unique?. Or is it just assumed, by the fact that the User exists, that it's unique?

This question is one that crops up over and over again in DDD communities. And every time all of the answers go on and on about "set validation" or "domain services" or "id reservation" or "insert some other abstraction"... All wrong. The answer is not what you might expect.

The uniqueness of an entity is not a business rule!

Read that again. I'll wait for it to sink in...

The uniqueness of a domain object is a technical invariant. The only reason you need some sort of id value is to allow for persistence/hydration. That is, if you never had to serialize/unserialize your model you wouldn't need any id values!

As such, an id should be understood as a special value in your domain because it actually isn't there for your domain. It is a technical artifact. This doesn't mean that the value can't be subject to business rules, just that it's uniqueness is not one of them. Any PK or AK should be understood in these terms (i.e. a unique PhoneNumber between all Users is the same as a unique User).

With the above in mind, often the best solution is to just allow your Repository to throw an exception if a duplicate User is added.

I'll let the readers ruminate about how the above relates to the idea of aggregates, child entities, and local identifiers.

4
  • 11
    Disagree. Generally speaking, the uniqueness of some entities and/or their properties is explicitly required by the business. There's no technical reason I can't use my email address to sign up for multiple accounts at X. But, X may not want me to. Similarly for phone numbers, name + address combinations, social security numbers, etc ... The only unique property of an entity that the business shouldn't care about, because it does generally exist for technical convenience, is the internal ID associated with the record. But, technical convenience is not the only uniqueness requirement.
    – svidgen
    Commented Nov 11, 2019 at 17:30
  • 4
    I think I disagree with you in almost everything, except for the final recommendation: "...often the best solution is to just allow your Repository to throw an exception if a duplicate [Phone] is added. " Commented Nov 12, 2019 at 0:16
  • @svidgen I understand your perspective, but you may be misunderstanding mine. My answer is not asking a business to accept duplicate data. That is, I'm not suggesting "uniqueness" can't be a rule, rather, that it is not a business rule and needn't be modelled in our domain. For the vast majority of use-cases it makes absolutely no difference at all to the domain whether or not the data being marshalled around is unique (i.e. an email will still be sent to the correct recipient). This is doubly true for the kinds of data you are calling out because they are inherently unique. Commented Nov 18, 2022 at 20:39
  • @svidgen To illustrate this further we can imagine a diagram of a domain model. Now does every box represent a huge set of unrelated objects? Probably not. Because the domain model is a projection of the data model. Uniqueness is almost always only a requirement for the creation of this projection (e.g. queries/joins). This is clearly out-of-scope for the domain itself. Commented Nov 18, 2022 at 20:39
4

But what if we want this phone number to be unique?

An important question is "what is it worth to the business?" How expensive are errors here? Will a best effort approximation keep you under the error budget?

In most databases it's fairly easy to add a constraint that generates an error when trying to store a duplicate value. But when following that approach, the model doesn't explicitly make clear that a phone number should be unique. If the model is reused or the database is changed, these business rules could be overlooked.

That's the most common approach -- the general term for the uniqueness problem is set validation, and RDBMS systems tend to be really really good at sets.

So what that answer might look like is using an RDBMS as your message store, and also have in the schema a table for the phone number mappings, with the appropriate uniqueness constraints.

But as you note, that solution isn't entirely satisfactory, because the responsibility for managing the constraints is split between the storage appliance and the domain model.

If you want to solve this in the model, well... the answer is to pull the set into the domain model. So far, I've only seen two variations.

One is to make "the set" into an aggregate - so all of the users would be part of a single aggregate. Checking the uniqueness of the phone number is easy, but contention for other kinds of edits goes up.

Another is to make "the set of all users with this phone number" into an aggregate. Instead of "this user has a phone number", it's "this phone number has a user".

A big consideration in these cases, and the reason you'll get a lot of advice not to got down this particular rabbit hole, is that your system isn't the authority for the relationship that you are trying to enforce. Unless you are a phone switchboard assigning numbers to accounts, it's not your data, and it can change without giving you notice. (Note: you get the same problems when using the database to enforce constraints on data you don't own.)

If you pull phone numbers into model how you will handle concurrency, if someone adds the same number after you add it second time but just after you pulled set from db...

You'll handle concurrency the same way that you needed to anyway -- first writer wins semantics on your persistence store. You load an old value into your model, compute the new value, and then apply a compare-and-swap operation on your persistent store. The winner of the data race is done, the loser gets to compensate using whatever strategy you prefer (abort, retry, ignore)...

There's nothing about set validation that is special here if you remember that you need to model the set as a first class entity (which means paying for the contention).

Why would you prefer this over creating a Domain Service to check uniqueness before inserting/updating?

Because if you aren't locking the set against modification, your validation has race bugs.

Fundamentally, all of the checks you run in local memory are working off of a copy of the data. If the underlying source changes while you are looking at the copy, then you haven't really checked what you think you have.

I only know of two patterns to address this; you either put a hard lock on the set (which is what we are effectively doing when we arrange that the database enforces the constraint), or you apply an atomic compare-and-swap to ensure that the copy of the data that you used for validation is still up to date.

2
  • If you pull phone numbers into model how you will handle concurrency, if someone adds the same number after you add it second time but just after you pulled set from db... Too complicated, better keep set validation in db and simple rules in model. Ideally you should have full validation on every layer but that is well 'ideal'.
    – Mateusz
    Commented Feb 5, 2019 at 15:21
  • Pulling all the data to the model by creating a big aggregate is a third approach. Why would you prefer this over creating a Domain Service to check uniqueness before inserting/updating?
    – Rik D
    Commented Feb 5, 2019 at 15:55
2

the model doesn't explicitly make clear that a phone number should be unique. If the model is reused or the database is changed, these business rules could be overlooked.

According to description the phone numbers uniqueness is out of a user model scope. It is in a scope of all user models. Business rule should be reused in that scope, not in the single user scope.

The second approach requires more code and an additional round trip to the database, but it improves the domain model.

This approach explicitly expresses the business rule which should be in a domain service. Domain service is free to use repositories to check that rule. You mentioned UserService but it is not appropriate name since it means user's scope. User's scope should be handled inside of user model (e.g phone number length validation). Your service should be named like UserRegistrationService or smth like that. UserService potentially has a risk to turn into the God object.

If you use RDBMS as a storage for your model state then it should have unique constraint to guarantee that it is impossible to save duplicates as a result of race condition when two threads checked uniqueness of the same number simultaneously and then tried to save it. It is like if you would use in-memory collections instead of DB on a persistence level then you would use Monitor to sync threads and make sure that two same phone numbers never be saved

2

I could almost agree with king-side-slide's answer.

The uniqueness of an entity is not a business rule!

I say "almost", because this is not absolutely true when you define systems, meaning that you are free to impose any rule you like, as is common in DDD. However, when designing/abstracting a system, a business rule does not automatically make sense just by simply "wording" it. The problem stems from the fact that one may not be staring at their Model as a conceptual system in its totality, so some business rules may not be "worded" properly. Stating a business rule is one thing, but clarifying the interactions hidden behind the rule is another. Too simplified "wordings" usually convey too little information.

For starters, think about it in a simplistic manner...

Let's consider an entity: User, with various properties/attributes.

Let's introduce a Rule: Users must be unique.

The rule practically introduces a constraint. This constraint is not free of assumptions and definitely not a domain consideration at the same level with User entities. Think about it for a moment. "Users must be unique" automatically introduces an additional level of hierarchy! If users have to be unique, then, by definition, there are multiple users.

Ooops... we skipped another consideration first... There exist multiple users. So, you need a user repository, you may think, but do you? The answer here is "it depends". And at about that point, you start wondering... what am I talking about?

I will make the statement that if your users do not interact with each other or with some other system, then you don't need a repository, plus the uniqueness rule is completely irrelevant! Put in simple yet relevant words, users residing in different countries (or even cities) do not need unique phone numbers.

But...

If your users coexist in, or co-interact with, a system, then the system has to be a higher-order entity than users, plus it has to "know" the users somehow (typically by proxy identifiers, such as records). For example, a hospital contains medical records, and user duplicates must not exist. So, you have your uniqueness constraint, but you also have a hospital. See, where I am going with this? First, there is a hospital (a hierarchically higher-level entity) and then, there are User entities, representing, for example, the patients.

Once we get past the fact that uniqueness (as a constraint) is never imposed at the same level with the entities "suffering" from it, things become much clearer. The uniqueness constraint is there and it is real, but it is not a problem of the entities. In a sense, all "constructed" entities are unique anyway (both in the material world, and in the digital world, think about how you always use some kind of "new" statement to instantiate an object). It is the problem of that other higher-level entity/system that has to maintain proxy-records with respect to the lower-level entities. It is solely its own problem to make sure, not that entities are unique, but that its records, the ones that represent entities, are unique, in a sense that has nothing to do with the entities themselves.

Therefore, uniqueness is an artifact, indeed, and an implementation-detail relating to persistence. It is best implemented at that layer.

From then on, you just need to ask yourself again, what it is you are trying to achieve, as each specific problem has its own set of advisable solutions. For example, why do you want to have unique phone numbers? This constraint is unenforceable at the users themselves, who are going to be using your own system. That is because in most real-world models, users don't choose their phone numbers, they are assigned to them and, until assignment and upon assignment, phone numbers are simply artifacts, just like unique IDs used by databases. This ensures that, upon creation, phones are unique anyway.

However, you may have valid reasons for this, for example you might be registering "per-household", or you don't want a user registering using the phone of another user. But then, your Model should be clear. You do want uniqueness, of sorts, and you (should) know why you need it. In that case, your higher level system entity, the one "containing" the users (their records, actually), being the nicely-designed abstraction it is, will contain a Map of user-records "keyed" by various elements, with the phone number being one of the keys.

Think about this abstraction:

class PhoneNumber
{
    //Make sure this looks like a proper phone number,
    //i.e. constructed strictly by 10-digit numeric literals.
}

class ISP
{
    Map<PhoneNumber, User> UserRecords;
}

Can anyone miss the fact that Phone numbers must be unique for each user registered to the ISP when they come across this somewhere inside their Model? I presume not. Plus all the relevant information is there, in a single location.

In (very) short, the problem usually arises when not having put that much thought into what type of system is being abstracted. You may think about the uniqueness of your entities, but you are better off thinking about what purpose this uniqueness serves, and which (hierarchically higher-level) entity's responsibilities it falls within.

Also, because I have not stated the alternative I started with in mind, given that your problem is this:

In most databases it's fairly easy to add a constraint that generates an error when trying to store a duplicate value. But when following that approach, the model doesn't explicitly make clear that a phone number should be unique. If the model is reused or the database is changed, these business rules could be overlooked.

...then you appear to have primarily a problem of practicality (i.e. make sure maintainers do not forget to check for rule enforcement). So an acceptable alternative would be to just use some type of meta-data or Apps Hungarian-style notation to achieve this. For example, think about the following:

class User
{
    //...
    [UniqueField]
    PhoneNumber PhoneNumber { get; }
    //...
}

or

class User
{
    PhoneNumber UniquePhoneNumber { get; }
}

Yes... if someone stumbles upon this piece in some codebase, they will almost immediately get the point (I see, user phone numbers must be unique, this is a business rule), and they will probably remember to double-check the persistence layer for enforcement, but they will never know why, plus they will hardly get any idea about where this is relevant, which are the problems I have (probably over)stressed above. So, to conclude almost as I started...

Stating a business rule is one thing, but clarifying the interactions hidden behind the rule is another.

So you may simply need to better model the User phone number uniqueness inside the entities where it is directly relevant.

So, where does this leave us? Well... you asked:

In a hypothetical system that handles adding users, there are several business rules. Some of the rules can easily be checked in the model. For example a user registration can only be saved if they entered a 10 digit phone number.

But what if we want this phone number to be unique?

...if you share more information about why, and what purposes this is to serve, so that we can see the hidden interactions that this business rule entails, it might be possible to suggest more specific amendments to your Model, so that the rule can be properly enforced and immediately made clear to maintainers. Without looking at your Model as a whole, all I can suggest is something along the lines of:

class User
{
    //...
    [UniqueField]
    PhoneNumber PhoneNumber { get; }
    //...
}

This almost "screams" to the maintainer: "Wherever you store the users, make sure their phone numbers do not overlap".

1

“What if you want the phone number to be unique?”

What if it isn’t? Say I order something from your store, my wife orders something as well, and we give you the some address, the same phone number, the same debit card number. Are you refusing to accept orders from my wife because her phone number is not unique?

0

One key point of Domain-Driven Design is to use the language of your Domain to do the modeling. You said, your requirement is to have the user save a registration during which the phone number should be validated to be unique. That to me means:

public final class Registration {
    ...

    public void submit() { // Sounds nicer than "save"
        sql("insert...");
        // Handle uniqueness errors
    }
}

By modeling the actual use-case, we have a place together with the data where the validation may be delegated to database where it obviously belongs in this case.

I know where you are coming from though. Some people suggest that we should only use "pure" (or "clean") code to implement every piece of logic and we should abstract and generalize everything else. I don't see why we should re-implement stuff that is already there for us to use. There should be no shame associated with using actual database features.

5
  • What if the phone is nullable? How many unique nullable can we have in RDBMS?
    – Laiv
    Commented Nov 12, 2019 at 8:09
  • Why would it be nullable if one of the requirements is that it is required to be filled out. Commented Nov 12, 2019 at 8:20
  • The thing is, that while it's ok (and I agree) to take advantage of any of the components of the system (e.g DB constraints, security, etc), the trend is relying on them by default and that's a mistake (IMO). DB constraints are meant to ensure data consistency, not your business rules or policies. The phone is a horrible id and it's a candidate for substitution. If the phone becomes nullable, the UK constraint no longer works.
    – Laiv
    Commented Nov 12, 2019 at 9:47
  • Why did I make the comment? Because while I agreed with you and regardless of the meaningless example of the phone, the question is generic enough to say that we should think about the adequacy of delegating business policies or rules to other systems. I just missed that in your answer. Situations to think about that could make us not rely on the DB features. Right now, reading the answer, someone might think that we can just delegate such irrelevant validations to the DB not considering its adequacy. Coding business rules such as uniqueness is also ok and very valuable.
    – Laiv
    Commented Nov 12, 2019 at 9:53
  • I agree with you to some extent, that doing validations is ok in any component. However specifically uniqueness is a thing where the database is in a much better position to check. On the application side you specifically have to do a query for it, whereas on the database side you get it "for free". So I think it is not just adequate, but it makes little sense to re-implement such things in the application. Commented Nov 12, 2019 at 18:24
0

Given the fact that the interfaces of the repositories can sit at the domain layer, wouldn't it be feasible to have a method within the User Repository interface that does that uniqueness check? It can then be injected within the domain model logic, while the implementation can still sit at the infrastructure layer. This way you cannot have an invalid user domain model within the lifecycle of your application which would be deemed invalid when attempting to persist said domain model.

1
  • 1
    It could, but it would have the downsides of letting the repository handle that, without the benefits of it (consistency).
    – istepaniuk
    Commented Oct 16, 2020 at 20:26
0

I think leave to the RDBM the tasks it's created and optimized for. You can check some things before sending the data to the RDBMS for performance reasons, yet there are a ton of things the RDBMS does for you than just enforcing unicity. It enforces relational integrity, for example. Checks requiring set operations are the specialty of the house and should not be replicated on the model. I'd keep it simple and let the RDBMS do its work.

Besides:

We no longer live in a one-front-end-one-database world. So take advantage of the RDBMS so any of the different front-ends that may comprise a comprehensive enterprise solution don't need to enforce all business rules themselves.

If there's a service between your front end and your database, let the service capture database exceptions and return appropriate error values.

1
  • 1
    Relational integrity checked in a RDBMS does not seem to make a lot of sense in DDD. Neither does "front-ends" enforcing business rules. Aggregates are consistent inside their boundaries, by design, in code, in one place. If you truly drive your design with you domain, databases are to store data, not to enforce business rules.
    – istepaniuk
    Commented Oct 16, 2020 at 20:32

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.