Validating inner layers with Spring

Christoph Huber
sprang
Published in
6 min readOct 28, 2020

In my last post I described how to validate requests with Spring Boot and turn exceptions into consistent and readable http responses. Now I would like to describe how I usually validate the inner layers in an onion architecture and domain-driven design (DDD) approach. For demonstration purposes, I have implemented a small API, which you can find here: https://github.com/huberchrigu/bgg-api. It contains both Java and Kotlin versions of REST controllers, but only Kotlin code for inner layers. The reason for this is the difference between the two languages when using JSR 303 constraint annotations such as @NotEmpty. The example is based on the Webflux project, but all principles can be applied on Spring MVC as well.

Exception Handling

Turning an exception into an http response remains part of the infrastructure layer, since it depends on the web infrastructure and might look different for a non-web application. So all we need to do is turning business exceptions of the domain and maybe also service layer into infrastructure exceptions.

A domain exception could be as simple as this:

class BggDomainException(message: String) : RuntimeException(message)

All we need to do is to turn it into a ResponseStatusException, such that Spring responds with a Bad Request.

Validating service parameters

In the service layer, we usually check permissions, handle cross-cutting concerns like caching or transactions, transform input parameters into repository calls and transform repository data back into service responses. The most obvious pre-condition and sometimes also post-condition here are security checks. When we use @PreAuthorize and @PostAuthorize, the Spring Boot default already turns failed checks into a Permission Denied response.

In additiona to security, we must ensure that we prevent any unknown exception caused by invalid client input. In the web environment, an unknown exception will later be translated to an Internal Server Error, which should trigger an alert and error analysis. And this should not happen for invalid client requests, we do not even need to log a single line in this case (in production).

Often, input parameters have already been validated in the controller, so you may choose to skip simple constraint validation. Service responses on the other hand may require a post-condition, like “non-null”. A simple example where there is only a repository method that returns a list, but the service needs to return exactly one element, is

NotFoundExceptions can be turned into a Not Found response like described in the last section.

Ensuring that there is exactly one element is one of the few examples of validations that make sense in the service layer. Often a validation contains business logic. This is a great indication that parts of the service need to be moved to the domain layer.

Domain validation

The domain layer must ensure valid states among all domain objects. It must not be possible to modify the domain state such that it becomes invalid. There are different places where the state needs to be validated:

  • Constructors of all entities and value objects
  • Factories or builders that create entities or value objects
  • All modifying methods on entities (including setters)
  • Methods of domain services

Design your domain classes such that there is no way to violate any invariant. If you use a persistence framework (like JPA) that requires empty constructors, either use separate classes in the infrastructure layer or make sure that persistence classes are validated after creation.

Often developers are unsure about when to create domain services. Validation within an aggregate is an important aspect of domain services: If you cannot ensure an invariant within a single entity and the invariant includes business logic (and therefore must be part of the domain layer), you need to implement this invariant within a domain service. A typical case is validating a reference to another domain aggregate.

Example: Insurance premium

Consider the following example of a domain service that computes the new insurance premium when adding new products (covers) to an existing contract. Input parameters are the contractNumber upon which the premium shall be computed, the products to add (addProducts) and data that is required to compute the new premium (contractData). This data depends on the added products, since each product requires other data. So the data is validated against the new products. The validation returns a new type ValidatedProducts which is required to call the repository that returns the new premium.

If the repository method arguments were just contract, addProducts and contractData, it could be called from the service layer without making sure that the combination of addProducts and contractData is valid. Enforcing the new type ValidatedProducts guarantees that products are validated, which can only be done in another aggregate (product) in this case.

Constraints

I often find difficult to decide where to validate simple constraints such as “not null”. It can easily be done in the infrastructure layer with @NotNull annotations, in the service layer it is often needed to ensure valid repository calls, and of course in the domain layer constraints must be validated to ensure a valid state. So is it really necessary to implement the same “not null” constraint three times? Not necessarily,

  • languages like Kotlin support such checks natively
  • even though many DDD enthusiasts would disagree, I think in some cases it is possible to pass objects through multiple layers and annotate them with constraints in the innermost of these layers
  • you may decide to skip all validation checks in the outer layers and focus on the domain layer only (where valid states are mandatory), as long as all exceptions from the domain layer are escalated to the outer layers

Which option to choose depends very much on the nature of your software and environment. If you use Spring Rest Docs to document your API, use JSR 303 constraints in the infrastructure layer. If you write your code in Kotlin, JSR 303 constraints might be less useful since you cannot use Kotlin’s null safety anymore. Or you use JSR 303 constraints with nullable types on the outer layers and transform them into domain objects with non-null types.

Internationalization (i18n)

Instead of setting the error message in the BggDomainException, you could set an error code, which can be translated to a human-readable message in the infrastructure layer, for instance by considering the Accept-Language header:

fun handleBggDomainException(e: BggDomainException)= throw ResponseStatusException(HttpStatus.BAD_REQUEST, messageSource.getMessage(e.code, emptyArray(), LocaleContextHolder.getLocale()), e)

Summary

Java, Kotlin and Spring provide numerous possibilities on how to validate data and state in all three layers of a Domain Driven Design architecture. The best solution where and how to do validation depends on other parts of the architecture and development project organization. The only layer where validation is mandatory is the domain layer, in which business logic including validation can easily be unit-tested. Exceptions thrown due to wrong user input should be escalated to the infrastructure layer, where it can be translated to an appropriate (web) response.

Lastly, the following diagram summarizes the validation and exception handling for a typical http request flow through all three layers. The small numbers indicate http error codes to which exceptions at this place might be turned into.

  1. Deserialization: When parsing and deserializing request parameters and bodies, exceptions due to invalid types or formats may be thrown. They are normally turned into 400 Bad Request or 415 Unsupported Media Type http response codes.
  2. Constraints: Spring makes it easy to validate the deserialized objects against JSR 303 constraints. Constraint violations should result in an 400 Bad Request.
  3. Pre-Condition: The service must make sure that all input parameters are fine to be turned into a repository or domain service call.
  4. Business Validation/Invariants: This should be the main place to ensure correct state.
  5. Infrastructure Checks: Repository implementations do not validate input data, but infrastructure state like the availability of services and databases. This could result in specific server-side errors like 503 Service Unavailable.
  6. Post-Condition: A service may apply a post-condition like verifying that the domain returned at least or exactly one entity.
  7. All other occurring exceptions are unexpected and therefore 500 Internal Server Error.

--

--