This article originally appeared on the builtwithdot.net Blog.
Ever struggle with where to place your app's business rules and validation?
Shoul...
For further actions, you may consider blocking this person and/or reporting abuse
I feel like having "validation" in the constructor, or on the value object at all is at odds with evolving business rules.
If we have an escape hatch that skips validation when deserializing "known good", or rather "previously accepted" instances, means in our other logic we can't rely on the "invariants" being true, but without that being immediately obvious.
And always going through the constructor will just make our code break on old instances whenever we make the validation "stricter".
It's extremely common, unavoidable, really, to have an active
InsurancePolicy
that could no longer be created/issued with the current rules, and yet needs to be honored and handled until its end of life, which may be indefinite.Sure, there's this version issue when applying something like this. It also exists when using event sourcing, for example. It's a trade-off that you need to be aware of. Basically, you need to keep your aggregates backward compatible in the event the scenario you mentioned applies.
Just like most things in programming - it's about trade-offs. If your domain isn't that complex then DDD isn't going to be helpful.
If your domain is pretty complex, then the trade-off might be worth it 👍
I'd argue that the more complex the domain, the more likely it is the rules will change. That's why I prefer for the entities to be dumb, and the validation to happen in the command handler. This way the current rules apply to the "decisions" that are yet to be made, but old decisions are respected.
Just to make sure I understand 🙂:
All your individual use cases are represented by command handlers, and each command handler has its own unique set of validation?
In that case, if new rules of this kind appear but you are using let's say a DDD aggregate kind of thing, depending on the rule that needs to change, etc. you could just create a new aggregate model?
That way, commands that share the same rules can just use the same aggregates, and the ones that don't can just use different aggregates. I'm fine with that.
Either way, this and what (I believe) you said are almost the same thing. The only difference being that by using aggregates you just have the ability to share a grouping of business invariants/rules across handlers.
👍
I mean yeah, you do end up constructing a write model to understand if a command is allowed. And having the model allows you to, if needed, express some of your rules as assertions on the model. In which case, you could express validation as:
If we assume the command is valid, and apply to the in-memory model the events that handling it would persist, will the resulting state of the model still fulfill all the invariants? If it does, persist the events, and consider the command successful.
There are some good properties to this, such as writing your invariants as assertions, and this automatically applying to all handlers.
But in practice it seems very brittle, since, once again, the "state" of an old entity might violate the currently checked invariants... which, while not great and probably requiring some thought, doesn't really need to fail commands that are orthogonal to the violated invariant.
Looking at a real world example, having an incorrectly-formatted phone number/billing address is "bad". But failing all transactions to/from the affected account until the issue is fixed, despite all the prerequisites specific to the transaction being met, is much worse.
And, of course, there are a lot more reasons to fail a command than "succeeding the command would put/leave the state in violation of an invariant". And these rules that apply to the command and not the state still need to share logic.
Well written article! While I do like parts of the DDD concept, I'm not a fan of validation occurring on the creation of objects as it feels like one too many concerns for a constructor.
Take that final snippet in your post, the code doesn't actually read like it does any validation at all - it is just instantiating instances of objects. There is probably a case to say other objects do this too (eg. you can't create an instance of
Uri
that is invalid) but it just feels wrong (to me) having business objects like that.That said, it does eliminate the case that validation is accidentally missed.
I would agree that in scenarios that are not very complex there's overhead to this too.
But it really shines whenever you start dealing with complex business problems.
Some people prefer to do something like this, but instead of doing validation in the constructor they will expose some
Validate()
method.It's one of many tools 😂
One of the things I like in C# is actually the built-in validation system in the
System.ComponentModel.DataAnnotations
namespace. You can have basic things like whether a property is required or a string is meant to be an email address. You can easily add your own custom validation attributes too. There is a class calledValidator
in that namespace with aValidateObject
function which processes it all.The validation doesn't need to be constrained to the attributes either because if your object implements
IValidatableObject
, then aValidate
method on your object gets called too when usingValidator
.Keep up the good articles 🙂
Thanks! I've seen people do that too.
There's always the discussion as to how much should be validated using
ModelState
in .NET etc. too.I find it helpful, all things equal, to have the bulk of non-trivial validation located in the same place (inside the business logic - wherever that may be).
I've worked on a system that had to validate ALL business rules client-side, then in the app layer, and finally in the DB (stored procs).
So like the exact same business rules re-done in JS, C# and then TSQL. Really weird. But that's what they wanted!
I did learn about all 3 languages really quick 😂
You summed it up well.
Nice article! Can you elaborate about how to do validation when the data is outside the entity (in your scenario, for example, customer settings)
If the data is coming from somewhere else, but is required to do a particular task, then usually I would have some repository or something that will fetch the data I need and then include that as part of the value object's dependencies.
Sometimes, if this means returning too much data, then having a repository with a method that returns a
bool
might be a better choice?Usually, that's my approach; I have a validate method in the repo returning a tuple from there.
Is the "not prematurely naming your object" from the DDD book?
No, that was a tip I gleaned from Udi Dahan.
As a junior, I always wonder what is the best way to deal with complex business rules. This is really helpful for me to start exploring DDD. Thanks for sharing.
You're welcome. DDD is fantastic for, like you said, dealing with those really nasty complex rules.
For simpler stuff, just do something simple.
The beauty of designing software is that you can use different tools for solving different problems!