5

Suppose I have an inventory management software that manages an inventory of phones of many subsidiaries of a company. A business requirement for this software is that you can't register a phone with the same IMEI (a serial number) as another.

What I find difficult is where this duplicate check logic should be placed.

Right now, I have identified a Phone as an Entity and the PhoneInventory as an Aggregate Root for all the phones. I think that PhoneInventory should be the one in charge of making sure that there aren't duplicate IMEI.

Imagine I have the following code (in a command handler perhaps):


var inventoryFactory = new InventoryFactory();
            
var phoneInventory = inventoryFactory.GetInventoryOf("Company Identifier");

var phoneFactory = new PhoneFactory();

var firstPhoneCheckinData = new PhoneRegistrationData
{
  //...
};
            
var firstPhone = phoneFactory.FromCheckinData(firstPhoneCheckinData);
            
var secondPhoneCheckinData = new PhoneCheckinData
{
  //...
};

var secondPhone = phoneFactory.FromCheckinData(secondPhoneCheckinData);

phoneInventory.RegisterPhone(firstPhone);
phoneInventory.RegisterPhone(secondPhone); //Should throw DuplicatePhoneIMEIException

To make this snippet work as I expect I've tried the following approaches:

1st approach: Load all Phones together with PhoneFactory

This is the first obvious thing that came to mind because if I have to check for duplicates I must be able to look at all records. Although this approach might work in theory it is not feasible because it would load way too much data.

2nd approach: Inject an IDuplicateIMEIFinder in PhoneInventory

This approach requires that some of the business logic is offloaded to another class that will be used by PhoneInventory to enforce business rules. What I don't like is that now my Aggregate Root is dependent on some service that must be injected, making it hard to use ORM if PhoneInventory was to be saved to some data store.

3rd approach: Leave PhoneInventory alone and validate in application service

We could imagine that the snippet above is written inside a method of some application service called from the GUI and that the application service would call an IDuplicateIMEIFinder before registering the phones in the inventory. This approach is reasonable but now the business rule can be circumvented by simply not calling IDuplicateIMEIFinder.

4th approach: Pass IDuplicateIMEIFinder as an argument to RegisterPhone

I just don't like this approach because if my registration logic was more involved I might need to pass a lot of services as arguments making the method signature weird. Additionally, the caller now needs to get all the required services somewhere before calling the method.

5th approach: Let the factory do the job

This does not make sense to me. Mostly because I must put in place ways to make sure that all entity creation goes through factories.

At the moment PhoneInventory is not saved to the data store (only its "contents") nor does it contains all the Phones at all times. I'm starting to question its existence and its name as it feels more like a PhoneInventoryService rather than an actual entity, but I've kept it this way because it looks like it encapsulates the concept of an inventory for a specific company nicely.

I know that many variations of this question have already been asked but I just can't seem to find one answer that actually suit my needs. I don't get where I can put validation logic that involves looking at more than one entity and how to make sure that it will always be called.

1
  • 1
    A hashset of all IMEIs actually wouldn’t be all that big. The trick is keeping it in sync. Commented Nov 14, 2020 at 22:01

3 Answers 3

4

Quick feedback

1st approach: Load all Phones together with PhoneFactory

Not a great approach due to the sheer data needing to be loaded. Also, even for smaller data sets, it's not the right time to load it.

2nd approach: Inject an IDuplicateIMEIFinder in PhoneInventory

This is by far my preferred approach, more on that later. But I'll already suggest that thinking about it like a Validator or ValidationRule class instead of a Service can hopefully help with accepting this approach - even though that is a purely semantical argument.

3rd approach: Leave PhoneInventory alone and validate in application service

Validation rules should be inescapable, and this makes them escapable. Also, even if you assume good will from your developers, it's just a nuisance having to always make sure to reference the validation.

4th approach: Pass IDuplicateIMEIFinder as an argument to RegisterPhone

This is conceptually no different from approach 3, whereby the caller of RegisterPhone is the one who needs to decide what validation to run.

5th approach: Let the factory do the job

It's not the factory's job. Factories instantiate objects. Anything else is a violation of SRP.

Based on the posted code, I feel like you're overrelying on the factory pattern too much, but that's a different discussion for a different day.


Quick fix

Assuming your data provider supports it, you might be able to shortcut this particular issue and work with a unique constraint. This avoids having to load any data or run any other query to check for validity.

This is not the cleanest approach, since it shifts the business validation onto the data store. However, there can be significant performance benefits here that outweigh the imperfect implementation. I'm sure not everyone agree with that point, and I do also consider this highly contextual and very much dependent on the business and their priorities.

But I sense this question is more related to any kind of validation, not just uniqueness, so I'll continue the answer in a way that applies to any kind of validation that is not easily offloaded to the database provider.


Repositories are not aggregate roots

I think you've misunderstood what an aggregate root is.

I have identified a Phone as an Entity and the PhoneInventory as an Aggregate Root for all the phones.

A collection of aggregates is not an aggregate root.

When an aggregate consists of many different classes, there is one class that ties it all together. For example (and this is not your specific example), your Phone aggregate might contain Phone, SimCard (since phones can have more than one), PhoneAccessory classes that together describe the complete aggregate of what a phone is.

In this example scenario, Phone is considered the aggregate root, because you intend to access the SimCard and PhoneAccessory entities _via the Phone they belong to.

At the moment PhoneInventory is not saved to the data store (only its "contents") nor does it contains all the Phones at all times. I'm starting to question its existence and its name as it feels more like a PhoneInventoryService rather than an actual entity, but I've kept it this way because it looks like it encapsulates the concept of an inventory for a specific company nicely.

PhoneInventory is really just a service, so I suggest addressing it as such. It's left somewhat ambiguous here whether it's a domain service or datalayer service (= repository). Given that you consider it an aggregate root, I'm going to infer that it lives in the domain, and therefore it is a domain service.


Location, location, location

I think that PhoneInventory should be the one in charge of making sure that there aren't duplicate IMEI

I agree. This validation is part of the "registration" process of a phone, and therefore belongs to be in the method in which you register the phone.

However, when I say "it belongs to be in", I mean that the method should force the validation somewhere in its method body. The actual validation logic itself should ideally be abstracted into either a private submethod, but more preferably a class of its own.

Whether you separate the classes or not depends very much on how complex the validation is and how commonly you have validation in your codebase. Generally, I opt for validator classes because it makes it easier to adjust rules when it becomes necessary (either for testing or during the development cycle when requirements change).

What I don't like is that now my Aggregate Root is dependent on some service that must be injected, making it hard to use ORM if PhoneInventory was to be saved to some data store.

As established before, PhoneInventory is not an aggregate root. It's a service. It's still not 100% clear to me whether it's a domain service or a repository.

If it's a domain service, which I am currently inferring based on you calling it an aggregate root, then it can (and should) contain the business logic and business validation. The validator is just another kind of domain service on which this domain service depends.

If you think about it, it would be a bad practice magnet if there were to exist a rule about domain services not being allowed to be split up into multiple (sub) domain services when the complexity of the logic warrants it.
Therefore, you can logically conclude that it is not bad practice to have one domain service depend on another when they represent different responsibilities.

If it is in fact a repository, then it can still contain validation, but then it's no longer business validation. Datalayer validation is more related to limitations/requirements of the data store, rather than arbitrary decisions made by your application logic (= business logic). Since the data store is perfectly capable of entering the same IMEI more than once, there's no need for the datalayer to validate this uniqueness.

1

Orchestrating use cases with domain logic involving more than one aggregate is the perfect fit for a domain service.

So your PhoneInventory would be a domain service which makes sure that no two phones with the same IMEI are registered. In addition, if there is more business logic concerned with a phone inventory, for instance, handling a case where a phone already exists with the same IMEI, this domain service would be the best place for that kind of logic.

Although this approach might work in theory it is not feasible because it would load way too much data.

You are right that loading all phones just for the duplicate check would transfer an unnecessary huge amount of data. That is why the PhoneInventory service should utilize the PhoneRepository to perform the search for a phone with the same IMEI.

Just make sure that the PhoneInventory domain service is kept free of infrastructure dependencies. That is why it should be injected with an interface of the phone repository. The concrete implementation of the repository interface can than perform the data storage lookup in the most efficient way, no matter if you write raw queries, utilize an ORM, etc.

With such an approach you can also make sure that your aggregates (here Phone) will only contain logic that is limited to it's own boundaries.

3
  • 1
    But how can I enforce that PhoneInventory is always called? With this solution nothing stops me from getting a PhoneRepository and persist invalid phones
    – Gwinn
    Commented Nov 16, 2020 at 9:14
  • Well, there is also nothing that prevents you from directly writing to the database either bypassing all of your domain logic. It is about having a dedicated component that shall be called whenever phones need to be registered. It is about having this domain logic in at one place - here the phone inventory domain service. You can of course throw an exception from the phone repository as well if you try to add a phone with the same IMEI, that's up to you. But you should have the possibility to check first before exceptions are thrown and that's where the domain service comes in to play. Commented Nov 16, 2020 at 9:47
  • I have another question: if I consider Phone an AggregateRoot (which might be has I need to access a particular phone by Id so I need a repository just for the phone), wouldn't using PhoneInventory as a domain service leave Phone with almost no business logic ? I'm starting to feel that I cannot apply 100% DDD without breaking some rules
    – Gwinn
    Commented Nov 16, 2020 at 10:37
1

It’s a trade-off between domain model purity and domain model completeness. And performance as you already realized.

A complete domain model contains all business logic; no logic is performed in application services or elsewhere.

A pure domain model has no dependencies, directly or indirectly (via interfaces), to out-of-process system like databases. In other wordt, a pure domain model needs no mocks when testing it.

All of your suggested solutions violate one or both of these principles.

It’s true that a simple query to check if the IMEI already exists in the database would be much faster than fetching all records and storing them in the Aggregate root.

I would generally prefer purity over completeness.

There is a possible solution for this particular scenario which is both pure and complete and still performs well.

// command handler
bool phoneHasUniqueImei = ... // query db 
phoneInventory.RegisterPhone(phone, phoneHasUniqueImei);

By passing the bool to the Aggregate root, the domain can perform it’s business logic, which can be as simple as throwing an exception, or perhaps raise an event.

It might seem a bit weird. Isn’t the logic performed in the application service? Yes and no. The decisions that have to be made based on the uniqueness are still performed by the domain, these decisions is what we call business logic. How we determine whether an IMEI is unique isn’t relevant to the business. So it’s fine if that is performed in the application service.

4
  • This is an interesting suggestion because there's a functional-style flavor to it, and it's one step away from using the Maybe type. You have an application service that takes in the original Phone instance, queries the DB, and returns what is now effectively (or in spirit) a Maybe<Phone> (the { phone, hasUniqueImei } structure). In fact, the { phone, true } case may be a distinct domain concept, a "registrable phone" - domain experts may even have their own name for it. Commented Nov 14, 2020 at 23:17
  • One could even explicitly introduce a RegistrablePhone, and change the register method's signature to RegisterPhone(RegistrablePhone phone). Then to call the method, one must pass a candidate Phone to this DB-querying service to obtain a RegistrablePhone. This lets you avoid (or rather, write logic to prevent) calling RegisterPhone at all if the IMEI is not unique; IMEI conflit is IMO, not something that should throw an exception anyway, as it's not an unpredictable exceptional circumstance - it seems that it's actually something that the software is expected to handle and help users with. Commented Nov 14, 2020 at 23:17
  • @Rik D It feels a variation on passing a service to the aggregate root, but I get what you mean about preserving purity. What I don't really like is how easy it is to circumvent the validation: you just need to pass false
    – Gwinn
    Commented Nov 15, 2020 at 9:01
  • @Filip Milovanovíc I like your interpretation, I believe it models the domain better (maybe post it as an answer). I have a question: shouldn't be a domain service that returns a RegistrablePhone rather than an application service?
    – Gwinn
    Commented Nov 15, 2020 at 9:07

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.