Domain Object Validation - Jakarta Validation Annotations in the domain vs. Clean Architecture
As the list of prompts below shows, I made an initial decision to use Jakarta Validation annotations in the domain and let the boundaries invoke the validations using an 'Valid' annotation. It was a novel approach suggested by Claude, I thought why not. I was curious how it would look and was ready to go with it.
It helps to talk to humans at lunch to get other opinions! On the way home, I thought about Chpater 32 of the "Clean Architecture" book from Robert Martin. I had been kidding myself into thinking that writing a Guard.against(...) class to enforce my validations was too much effort, and that it would be more efficient to just use Jakarta annotations to validate my domain records.
So, in one big commit, you can see that the domain is now independent of external frameworks. As this post involved a lot of prompting, I will end it here. No new shiny features, but a step forward.
Prompts Used (full transcript):
- "we are going to work on a blog post and the most important part of the application - our domain. Before we get started, lets define in the claude.md how our domain objects. these records require validation annotations like @NotNull of @NotEmpty depending upon the business rules. For instance a BlogPost must have content so the BlogPost must have a @NotEmpty String content element. where would this validation take place that the parameter provided is not null or empty?"
- [Selected: Application service layer - Services use @Validated class annotation. Validation happens regardless of which adapter calls the service.]
- "great - this is a new approach for me - typically I validate in the domain using a contraintsValidator.validate(this) in the domain record itself. I'm excited to validating at the application/domain service level will play out. write the blog post and comment domain objects using annotations that align with the already provided database limitations. for instance, title is not empty and maximum size 255 characters. Use the philosophy that everything is an object - Title has its own record that is used in the Post. all live in the blog package. Any questions? Perhaps its best to just git it a try and we tweak it afterwards. Each domain object requires a UnitTest. We do not use Mockito in our domain. The unit test for a domain object should always have a validateConstraints() test. If there are multiple parameters in a record constructor, then validateConstraints should be ParameterizedTest and the arguments should be fed from a static validateConstraints function in the same test."
- "pretty good - under no circumstances should the primary validateConstraints test have a method source that isn't validateConstraints..."
- "null and empty values should not be added to the arguments tested in validateConstraints(). Better to use org.junit.jupiter.params.provider.NullAndEmptySource"
- "In the test code: com.javablog.domain define a class Fixture.java..."
- "ok, that worked. next topic - we want to no longer inherit from the spring-boot-starter-parent..."
- "great that worked fine. now lets introduce the maven-dependency-plugin:3.9.0..."
- "actually - make failOnWarning false and run a maven build to capture all of the warnings..."
- "remove warnings.txt and build-output.txt. update the tests to use the Fixture instead of constructing the records individually."
- "I've changed my mind after talking to a colleague today and do not want to try a variant of the same solution I've been using. lets keep the unit tests, but change the ways the validation is performed. Remove all annotations from the domain records."
- "I did not say to delete the validation tests - just the annotations. leave the tests. stop the procedure now."
- "alright - the domain is going to use an Uncle Bob classic 'Guard' class in the com.javablog.domain package. This guard class has a static function: Guard.againstNull(Object value, String fieldName); Instead of using an @NotNull annotation the compact constructor should be defined without parameters and refer the value, for instance in Title, to the Guard.againstNull(value, 'Title.value') implementation. If the object in againstNull is null - it should throw a ConstraintViolationException, a domain defined class that extends RuntimeException and prints a pretty message based upon the 'Title.value' helper and the context of the throwing function. write the Guard, apply it to Title, update the test to use assertj to assert the exception is thrown with the appropriate exception instance and message."
- "its too early to execute the maven, the other tests need to be cleaned out first. change the other domain objects to use the Guard where appropriate. Add an additional Guard.againstEmpty. And Guard.againstInvalidLength. that should encompass all of the validations we were using. Update the domain classes to use these guards where we were using Annotations before. Remove the static of() and other static constructors I didn't ask for."
- "remove the rest of the static of methods from the rest of the codebase."
- "haha - have a look at JpaBlogRepository"
- "Instead Of the BlogRepository.listPosts() returning a list of posts it should return a Posts object which takes a Set<Post> values in its constructor. The set may be empty, but not null."
- "apply the same pattern to Comments. use a 'plural' domain object and its not allowed to have null values."
- "write unit tests following our conventions for Comment, Comments, Author, Post, Posts"
- "In Guard.java - there should be no value value != null - reuse the againstNull. Introduce a simplified againstMaxLength(String value, String fieldName, int maxLength)"
- "remove it. also change the name to match the convention instead of againstMaxLength use againstInvalidMaxLength"
- "ok, update the list of prompts we used in this session in home.html. I know it will be long, but we have a plan. Also update the domain claude.md to reflect the validation rules and how tests are written."
- "one last prompt. in the home.html create a css class for the prompts - light grey. Create a horizontal divider in between the posts - like an hr but it could be a border bottom."
Comments
No comments yet. Be the first to share your thoughts!