DDD – How to implement validation against database
Asked Answered
M

2

3

I'm struggling with a basic issue. The project is in C#, but the issue is general.

I'm following the always valid object principle. As one example I have a product entity that has a mandatory property "ProductCategory". The allowed product categories are user defined and therefore persisted (in a database).

For type safety and better reading I defined Value Objects for various properties to encapsulate the business rules for those types. So there is a ProductCategory class. When creating a ProductCategory instance a factory method checks for example the max length of the string that is passed to the factory before creating the instance. This ensures that each instance of ProdcutCategory is valid.

Simple parameter checks like length are easy and straight forward. My question is where to implement the validation check against the possible values that are persisted. A repository for the allowed values hides the persistence technology and has a method Exists or IsValid.

Option 1) calling the repository from the application layer before calling the factory.

Here the domain layer has no dependency on the repository/infrastructure layer as many advocate. But the factory can't ensure a valid object anymore. It depends on the application layer that the business rule is implemented. Also each command that needs a ProductCategory has to check the repository, which violates DRY.

Option 2) calling the repository from the value object factory

Since factories are part of the domain layer the layer must have access to the repository, which introduces extra coupling. The benefit is that the business validity of ProductCategory is ensured by the object itself and can't be circumvented.

Is there another solution to this dilemma or are there any specific criteria that encourage one or the other option? Would it be OK, if the factory is programmed against an interface of the repository that is saved in the domain layer and only implemented by the repository in the infrastructure layer? I have tried to understand the different approaches, but in this case I feel lost.

Mitrewort answered 18/10, 2021 at 11:42 Comment(0)
I
5

If I understand correctly, your ProductCategory value object takes care of its own invariants, but whether a particular ProductCategory can be assigned to a particular Product is user defined and stored in the database.

You could use DomainEvents:

  1. You assign the Category to the Product in the Domain model.
  2. The Product adds a DomainEvent "ProductCategoryAssigned".
  3. Before committing your unit of work a DomainEvent handler performs the check with the databse that the particular Category can be assigned to the Product, and throws if not.

This will prevent the transaction completing with an invalid state.

You could use ValidatorAggregate:

If you really want your Product to be "always valid", even before the unit of work is committed, then you could go for:

  1. Create a ValidCategoriesAggregate and Repository that loads the valid Categories based on Product properties.
  2. Your Product's "SetCategory" method (or factory method) declares a ValidCategoriesAggregate as a required parameter, forcing your application layer to get one before calling the factory method.
  3. In your factory method, you can call "ValidCategoriesAggregate.HasCategory(category)" and throw before creating the Product if not present.

Try to avoid injecting repositories into your domain model.

Incredulity answered 18/10, 2021 at 12:12 Comment(7)
in the case of domain events option, that check should be in-process and in sync?Kristianson
Using the ValidatorAggregate, what happens if the same category is created (committed/stored) just after your repository has loaded the ValidCategoriesAggregate?Sardis
@deezg ... Yes, if you have infrastructure that delivers the commands to your command handlers in the application layer, then this can then enumerate and handle domain events (that were created during the processing of the command) in-process, and send those off to event handlers before the unit of work is committed.Incredulity
If I understand correctly, in the case of domain events the busines logic for checking the validity of the category would not be the responsibility of the domain layer anymore, but of the application layer. Shouldn't be all business logic be in the domain layer?Mitrewort
The second option to use a ValidCategoriesAggregate as a parameter works if the list is small, but how would you do this to check the uniquness of an user email address, which would need to be checked against all addresses in the database. With millions of users it is practical unfeasable to load all email addresses into memory.Mitrewort
@M.Koch ... For that scenario, I'd use DomainEvents. Then the validation is just an "IF EXISTS" Sql query called from the Domain Event Handler, instead of retrieving all Emails into memory.Incredulity
@M.Koch To your point about validation in the application layer. Sometimes it is not possible to perform the required logic inside a single aggregate without heavy costs (such as retrieving a lot of data for the purpose of the command). For this scenario, there is a clear layer 'between' Application and Domain. In this layer you would put those bits that require some Domain knowledge and access to the infrastructure. It might include Domain Event Handlers, and Domain Services. This layer should be used sparingly to prevent an anaemic modal, but is a perfectly valid concept in DDD.Incredulity
S
1

Option 2 seems the better one.
Coupling is not an issue in this case, you are coupling your factory to the query needed to check validation, making this dependency explicit.

Watch out that the only way to not have this checks eventual consistent is having all the data inside a single aggregate. Otherwise there's no way to prevent the fact that the same category is created just after you did the check and before you commit your transaction (this is the real reason of aggregates).

Mind about eventual consistency only if there's a real issue in having it, how often a category is created? how often a product is created? how much is the possibility of a conflict to happen? is there some real issue if that happen?

Most of the times being full consistent is much more expensive than accept eventual consistency.

Sardis answered 18/10, 2021 at 22:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.