During one of our periodic code review sessions, the topic of input validation in our application was brought up. Whenever our API receives input from external clients we validate it according to a set of business rules.
For example, we make sure that field match certain patterns, that certain fields are mandatory. Also, we check that values are compatible with our business rules. Most of this validation is in our controller layer, since that’s the place where our input is received. However, our functional testing component doesn’t use this controller layer, but uses the service layer instead. Because of that, some important validations are skipped while testing. This triggered the notion that it seems something is wrong.
So, let’s take a step back and see how we arrived at our current situation. After that, we’ll see what we can do to improve our design and solution.
What is a Hexagonal Architecture?
To better understand the problem we’re addressing and its eventual solution, it’s helpful to be familiar with the concept of a Hexagonal Architecture. While the name is a bit of a misnomer (Polygonal Architecture would have been a more accurate name), the point of the architecture is that it has many sides, and each side represents an entry into our application. A different name under which is this Architectural pattern is known is Ports and Adapters, where a port can be seen as a connection from the outside world into our main business logic.
As an example, one might expose some business logic over REST as an interface, or a binary interface such as gRPC. Other interfaces can be dependencies of the main application, such a queues, databases, or other services. As such, we can make a distinction between incoming and outgoing ports.
The core of the Hexagonal Architecture consists of several layers. The main layers are our core domain, the domain services, and the application services. Outside of our application core, we have the infrastructure layer, which we can specialise even further into 2 parts. The first part is the incoming ports, which is generally known as the interface layer. The other part are the outgoing ports, or dependencies, which is the infrastructure layer.
As described in the introduction, in our software, the current validation is in the controller layer of our application. This is part of the Interfaces layer. While this approach is not unique to our application only, the controller layer is not part of the business layer (Application Core). Therefore it should not be modeled as such.
Instead, the controller layer is part of the interfaces layer, and should be treated as such. Depending on your programming language, it’s generally better to not put your controllers in your domain package. Instead, consider them as part of interfaces layer and package instead.
Where to place input validation?
A possible solution to address our testing issue is to change our functional tests. They need to change in such a way that they use the infrastructure layer instead of our service layer. While this might make our tests slightly more complex since we have another layer to test, it would increase the coverage of our tests. We would be able to test our validation layer using our functional tests.
However, one of the downsides of this approach would be that if we introduce another input layer we might end up duplicating some logic. Examples of this could be gRPC, JMS, or maybe even another REST endpoint. If we implement the validation in each of these layers, we’ll end up with duplicate code. This introduces possible bugs by having a slightly different implementation in each layer.
All of the above seem indications that instead of having a testing issue, we have a design issue. Without having to re-implement the validation in each endpoint, how can we come up with a better solution?
A better place for business validations is in the business layer. In this case, the solution involves moving the validation one layer deeper, from the controller layer to the application service layer. As mentioned before, the controller layer is an interface layer, and is therefore not part of the business logic. As such, this layer should not have any business concerns, such as validation.
In terms of code, the difference is subtle. The bulk of the change involves moving the validation to one layer deeper, to the application service layer. However, imagine now that there’s an extra endpoint, for example, a gRPC one.
If we’d follow the current approach in which we validate our input in the controller, we’d have to make sure that in our gRPC endpoint, we’d implement a similar validation. However, when moving our validation to the service layer, since our validation is in same service component used by gRPC, we don’t have to concern ourselves anymore with making sure we implement and test the right validation. The only thing we’d have to do is make sure we properly handle the validation errors in a way that gRPC can understand them.
In summary, in all but trivial applications, it’s recommended to move your validation logic to a common business layer. This improves the design of your software and has the added benefit to prevent possible bugs and duplication.