While shopping for Bean validation frameworks, we stumbled-upon these well-known ones:
-
Java Bean validation: This is only suitable for simple data validations like
@NotNull, @Min, @Max
. -
Spring Bean validation: It comes with a lot of * Spring-baggage* and works with Spring REST. But we predominantly use Connect framework on core.
Annotations are reflection based, and they create a lot of runtime magic. They are not bad in-general, but using them for validations has these cons:
-
It’s difficult to debug as you wouldn’t know which
AnnotationProcessor
handles which@Annotation
unless the Javadoc writer of that Annotation is gracious to provide those details. -
You can’t use a simple ⌘+Click to know what’s going on underneath anymore.
-
Annotations offer limited type-safety. It’s not possible to specify contextual requirements. Any annotation can go any type.
-
Use of Reflections for Annotations also incur a runtime cost.
-
Annotations are not testable.
Let’s understand what kind of validations can services that belong to the same domain have.
We have a group of services under Payments-Platform domain, such as - Authorization, Capture, Refund, Void. Similar service groups exist in Tax, Billing, Invoice domains too. All of these are REST-APIs that accept JSON payload. Services that support batch accept list of JSON sub-requests. A simplified version of a batch payload looks like this:
[
{
"amount": 99,
"accountId": "{{validAccountId}}",
...,
"paymentMethod": {
...
},
...
},
{
"amount": 77,
"accountId": "{{validAccountId}}",
...,
"paymentMethod": {
...
},
...
}
]
This JSON structure gets marshaled into a Bean/POJO, which needs to be validated at the entry point of our application
layer. Since all services in this domain deal with similar fields, they have a lot of common fields
like amount, accountId
etc., as well as common member nodes like paymentMethod
in their structure. Based on the type
of field, there exist 4 kinds of validations. E.g.:
-
Common Validations - for common fields that exist across services, such as
amount
,accountId
. -
Nested Validations - for the member nodes like
paymentMethod
. These nested members share an Aggregation/Composition relationship with their container and have validations of their own. A service in the same domain may reuse this data-structure in its payload. Such service, along with its own validations, needs to execute all the validations of this nested member.
Now that we talked about types of validations, let’s understand the requirements for validation orchestration (how to execute these validations).
-
Share Validations: Instead of rewriting, Share Common and Nested Validations among services that share payload structure.
-
2 Routes - 2 execution Strategies: Our database entities can be CRUD through two routes i.e., Connect and SObject. They both need to be guarded with Validations. But the tricky part is - the Connect-route needs to fail-fast, while the SObject needs error-accumulation.
-
Configure Validation Order for Fail-fast: A way to configure Cheaper validations first and Costlier later. Costlier validations can include Effectful validations, so we need to fail-fast and avoid unnecessary DB calls.
-
Partial failures for Batch APIs: An aggregated error response for failed sub-requests can only be sent after valid requests are processed through multiple layers of the application. We have to hold on to the invalid sub-requests till the end and skip them from processing.
-
Meta-requirements:
-
Accommodate a century of validations across a domain
-
Unit testability for Validations
-
No compromise on Performance
-