Validating business logic in a RESTful API

Validating business logic in a RESTful API

When trying to perform an action – especially one that changes the state of the system – in an API the request often needs to be validated. The question is, what validation should happen where?

The DB can help with enforcing certain rules (existence, uniqueness, etc.), but not all. Some of the validation needs to happen in the code. "in the code" is too vague though, where exactly should it happen?

The code

I have a repository where I am experimenting with different concepts/thoughts, this post is leaning on

https://github.com/mn-experiments/examware/tree/blogpost-1-rest-validation.

It's a driving exam management API, it's simple and kinda rough around the edges, the idea is to polish it as I cover more topics in this blog. We will use it to ground this blog in something concrete.

Proposal

Validation must happen immediately before any change of state in the API.

Here is the Attempt#save (link) method from the examware repository.

@Transactional
    public Attempt create(AttemptCreationRequest creationRequest) {
        var student = studentService.retrieve(creationRequest.studentId());
        var exam = examService.retrieve(creationRequest.examId());

        var attempt = new Attempt(student, exam, creationRequest);
        validator.validateCreation(attempt);

        return repo.save(attempt);
    }

The validation code is right above the JpaRepository method call. When looking to see where the attempt is being saved to the DB you automatically notice that it is being validated right before the save. There is no need to worry about the validity of the attempt, the answer is right there.

Alternative 1. method with lambda validation

public Book create(Attempt attempt, Runnable validation) {
    validation.run();
    return bookRepository.save(attempt);
}

This way you still get to enforce the validation directly before the state-change but with less coupling. It also becomes easier to write tests for the outer save method since instead of using mocking libraries for the validator you can just supply a simpler Runnable.

Extending Runnable can add an extra clue that we are dealing with validation here.

The convenience makes this approach less robust though. 9 invocations might be passing the correct Runnable to the method and the 10th unintentionally does it wrong. Changes to other parts of the codebase can affect the result of this part of the codebase, this is exactly the problem we are trying to address in this blogpost... not good.

Alternative 2. constructor with lambda validation

Building on top of Alternative 1. you could pass the Runnable or the actual Validator to the Attempt constructor. This way you know that an attempt can only be created if that Runnable allows it to.

This approach also has the weakness of Alternative 1. but there is one more wrinkle. When updating and creating Attempt s this works fine but when deleting I might also want to do some validations. I am not going to be instantiating a new Attempt though. In that case the validation needs to happen differently and that is not nice, is it really necessary to have 2 ways of doing business logic validation?

Wrapping up

I like this approach so I'm going to stick with it for a bit and see if I can poke any more wholes in it or find a better alternatives.

Quick rundown of the findings

The benefits:

  • Code review becomes easier. the expectation of having validation right before a DB, Kafka or Redis operation requires less concentration to check.
  • Easier to explain to new team members. You might have all your Controllers and Services and Adapters and Ports but where does validation fit into all that? It is much simpler to just agree that validation should explicitly happen in the same code block as any state changing operation.

But there is one point of contention.

Some people choose to split the DB representation from the objects in their code. In Spring Boot this means that they have one class that is annotated with @Entity, the AttemptEntity (this class is aware of the fact that we have a DB) and then another class which is a POJO (plain old Java object) Attempt (not aware of the DB). In this case one can bring up the criticism:

You are only validating the Entity now! What about the POJO? It's in the "core" of your application it must be validated!!!

It's a question of what should the source of truth be. Is it the model, or is it the persistence (filesystem, hashmap, DB, Kafka, whatever).

In my opinion, the persistence is the source of truth. Until you actually save the object, it doesn't really exist. Your validation is fine but the DB glitched so the save failed anyways – the magical "core" is not in control.

So, a fetched Entity is the reliable source of truth here. In Spring Boot it is unwise to pass Entities around your codebase freely though. If the Entity is managed – it was retrieved from the DB, for example – and you have some overarching transaction, then any changes you do to the Entity in your code will end up in the DB. This can go bad very quickly. I think the availability of Entities should be limited and that the data within the codebase should be passed in the form of DTOs (Data Transfer Objects) as much as possible.

This way we achieve the following things:

  • Only Entities and DTOs, no Models
  • Entities are explicitly the source of truth, no blurry lines between Entities and Models
  • DTOs are only constructed from persisted entities -> DTOs are valid "truth snapshots"
  • We use DTOs within the codebase instead of Entities -> no need to worry about side effects and truthfulness of data.