ThreeDotsLabs / wild-workouts-go-ddd-example

Go DDD example application. Complete project to show how to apply DDD, Clean Architecture, and CQRS by practical refactoring.
https://threedots.tech
MIT License
5.24k stars 479 forks source link

External validation against rule: Always Keep a Valid State in the Memory #65

Closed linfengOu closed 1 year ago

linfengOu commented 1 year ago

Hey there! Thanks for the work. I've been utilizing your methodology in building my own project for near 2 years.

However I still have a question about this rule when external validation is needed: always keep a valid state in the memory : Say if I have a big list of accounts, and they are organized in parent-children relationship, i.e. an account can be a top level account, or it can be child of a top level account. I have this Account entity in domain layer and a NewAccount method which is supposed to create a valid Account entity:

type Account struct {
    id uuid.UUID
    parentAccountId uuid.UUID
    ...
}

By following the rule, I have to verify if parentAccountId is valid in the NewAccount method. But how? It requires to check database if this parentAccountId exists or not. Seems not possible in domain layer.

Right now I'm putting this check logic in repository layer, but seems a bit violating the rule : (

There are also other scenarios I can think of that maybe certain fields requires remote call to external system to valid - it requires services in application layer - it's not accessible from domain layer.

Any suggestions?

Thank you!

m110 commented 1 year ago

Hey @linfengOu! Glad to hear our ideas were useful! :)

I think checking it on the repository level makes sense most of the time. After all, your domain code doesn't know where the parent account is stored and doesn't need to care. From the domain point of view, a valid parentAccountID would be "not empty" and this is what you can check in the constructor.

It's similar to things like enforcing unique email addresses. It sounds like domain logic, but it's just more practical to have a UNIQUE constraint on the database level and check it there (if your database supports it). Similarly, if you're using a relational database, you could have a foreign key constraint that won't allow using parent accounts that don't exist.

In a sense, this becomes part of the repository contract, even if not explicitly stated. You could include it in the interface comments, for example. Something like "Save saves the account, making sure the parent account exists".

When in doubt, I like to remind myself that repositories are not just dumb database proxies. They deal with the domain entities and have knowledge about them.

But if you wanted to do it the other way, you could also have the application command validate it. Or even a domain service or an "account factory" injected with something that checks if the account exists. As long as you hide the "checker" behind an interface, you should be fine. :)

Hope this helps!

linfengOu commented 1 year ago

Thank you very much @m110! I like your point about repositories are not just dumb database proxies. They deal with the domain entities and have knowledge about them.

I love the the idea to make check logic an app command as well. I'll give it a try.

Thanks again, and happy coding!