Our team is starting a new project following Domain Driven Design (DDD). At the high level, we have an API on the top of our domain which enables a client to perform operations on the domain. One of the question I'm not very clear about is where do we perform validation on a certain property/ attribute in DDD.
Consider this example. Let us say, I have a below data contract/ DTO exposed by my API:
public class Person
{
public string Email { get; set;}
public string Name { get; set; }
}
Now, let us say we have business validation which prevents user to enter an invalid email address and restricts user to have name more than 50 characters.
To achieve this, I can see following three approaches:
In Approach 1 , we do data validation only at the API (either through Data Annotation or Fluent Validation). I do not repeat the validation in my domain. Theoretically, this may mean my domain could go in an invalid state. But, since entry point (API) is being validated it is not possible in a real scenario.
In Approach 2, we do data validation at both API and in my domain. This approach helps us to completely remove coupling between my domain and API. The API can independently return a Bad request to the client. And since the domain performs the validation again, there is no chance of domain going to an invalid state. In this approach however, we violate DRY principle.
In Approach 3, we do validation only at Domain and do not perform validation on DTO at API level. With this approach, while we are not repeating the validation, the domain cannot throw an exception when API call tries to put it in an invalid state. Rather, we would need to wrap that exception in some Result
object. This would help the API to send an appropriate response to client (eg. Bad request instead of Internal Server error). What I do not like about this approach is that I would prefer to throw a hard exception rather than putting a wrapper.
Ask
Which approach makes the most sense and why?
Where is the line between a business validation and a business rule? (Assuming the business rule exists in domain).
Is there anything obvious which I'm missing here?
Note: This question may look similar to Validation in a Domain Driven Design n and where should put input validation in Domain Driven Design? but it does not really answer the specifics.
Implement validations in the domain model layer. Validations are usually implemented in domain entity constructors or in methods that can update the entity. There are multiple ways to implement validations, such as verifying data and raising exceptions if the validation fails.
Messages in Context Validation. Messages is an object that represents the validation state builder. It provides an easy way to collect errors, warnings, and information messages during validation. Each Messages object has an inner collection of Message objects and also can have a reference to parentMessages object.
Domain-Driven Design(DDD) is a collection of principles and patterns that help developers craft elegant object systems. Properly applied it can lead to software abstractions called domain models. These models encapsulate complex business logic, closing the gap between business reality and code.
DDD provides a set of patterns, terms, and good practices to build software from complex domains. It helps the developers manage the complexity they face when they design a solution. Another term we should define is domain model, which is just an abstract term.
I have gone through the answers provided by the experts here and after much deliberation within the team we have decided to implement the validations with below key principles in mind.
We treat the API validation and Domain validation/ Business Rules as separate concern. We put the API validation same way we would have without a DDD. This is because, API is just an interface to talk to our Domain. Tomorrow, we could have a messsage bus sit in between but our domain logic would not change. The API validations include but not limited to field length checks, regex checks etc.
The domain validations are limited to business rules only. The field level validations like length check, regex etc which do not mean anything to business are kept out of business rule. If there is a business rule violation we simple throw a DomainException
without caring about how consumer would handle it. In case of an API response, it would simple mean returning status code 500
back to the client.
Since, we are now treating API validations and business rules as a separate concern, it does mean that there are chances of repetition of some validation rule. But we are OK with that as it makes things much simpler and easy to reason about. Since they are loosely coupled we can easily change API validation or business rule independently without impacting the other.
There are few border line cases like email
where one could have argued to have ValueObject
in our Domain. But we take this on case-by-case basis. For example, we do not see any value to have a separate ValueObject
for email
field in our Domain at the moment. Hence, it is validated (regex check) only at the API level. If in future if business comes up with additional rules like restricting to a specific domain, then we may revisit our decision.
The above principles in mind have helped us to keep things simple and easy to reason about.
Now, let us say we have business rule which prevents user to enter an invalid email address and restricts user to have name more than 50 characters.
Important things to notice in this example
Semantically, both of these values are basically just flavors of "identifier".
That being the case, the domain model doesn't care about the validation at all, except for such problems like not running out of memory. Your data model may care, if you have fixed length columns or something like that.
So this could very easily be one of the places where you care at the message boundary, but not within the domain itself.
But it's not a good proxy for the general question of where validation might live.
Contrast this case with something like a deposit amount -- it's a number, and you would reasonably expect to add/subtract it from other numbers, compare it to other numbers, and so on. There, you might look at something like Integer.MAX
, and reasonably conclude that an attack/data entry error is so much more likely than a genuine use case that you will eliminate that option altogether.
Validation at the message boundary is primarily driven by the question: can you trust the source? If there is any doubt, then there is no doubt. (Deogun and Johnnson on Domain Driven Security is a good starting point).
Largely, validation at the message boundary comes down to establishing that the sequence of bytes that you have received is actually compliant with the message schema; which can of course include limits on the range of allowed values. (Example: HTTP Responses include status codes, but you aren't required to pretend that a response with status code 777 is meant to improve your afternoon).
And so it's a perfectly reasonable thing to declare that the name field in a message be no more than 50 characters, and that the email address field in the message conforms to the definition of addr_spec in RFC 5322.
And then at the boundary you make sure the bytes you get actually satisfy the message constraints, and pass it long if it does.
But within the domain model? if you don't need to make assumptions about the data, then done. "The application said these are bytes? Good enough for me!"
Somewhat more technically - the key test is whether or not the domain model has any preconditions that need to be satisfied in order to ensure the correctness of its results. If we have preconditions, then validation serves as a controlled way to detect violations.
But there's not a lot of value-add injecting the domain model with precondition checks that it doesn't need.
(Again, contrast with amount
-- the domain model has a lot of interest in detecting violations before it starts moving money around indiscriminately).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With