On validations and the nature of commands

by Paweł Świątkowski
05 Feb 2025

Recently I took part in a discussion about where to put validations. What jarred me was how some people inadvertently try to get all the validation in a one fell swoop, even though the things they validate are clearly not one family of problems. This led me to think about different kinds of validation and how does that relate to the nature of a command.

When talking about commands, I don’t mean any particular pattern in code. Throughout this article a command means an intent for some change to happen. In fact, let’s eject ourselves from the web app world for a little while and talk about commands in the situation when one person tells another to do something.

Say, Alice is telling Bob: “Fetch the broom from the garage and sweep the floor in the kitchen”. This is a command. A valid one. Bob can now start doing things to fulfil Alice’s wish. But it’s not always like this. Let’s consider some other scenarios of Alice giving a command to Bob.

  • The command might be expressed in Nahuatl, a language that Alice knows but Bob does not. Or it might be a gibberish. The point being that Bob cannot even understand what Alice is saying.
  • The command might be understandable, but carrying some unrealistic figures. “Get me 1500 bananas from the kitchen.” Even though technically possible, there is no way that there is that many bananas currently in the kitchen and Bob knows it. He might safely challenge Alice’s sanity.
  • “Grab a pickaxe and peel the potatoes.” The command can be structurally correct, but still make no sense.
  • A precondition might be not met. “Drive to the supermarket and buy milk”. “Okay, but give me the car keys”.
  • Finally, Bob might know that Alice is in no position to give him orders and refuse.

This is one class of problems with commands, but there is another: when Bob starts to perform it, but encounters a blocker. Maybe there is no broom in the garage. Or maybe Charlie is already sweeping the floor in the kitchen when Bob arrives there. Perhaps The broom breaks when Bob starts. Or maybe the floor in the kitchen turned into lava and sweeping is not possible.

In any of these cases Bob understood the command, acknowledged it and started to do things, but then had to stop before finishing.

There is, of course, a grey area too. If the house has no garage or there is a carpet in the kitchen - Bob could have known it, because he lives there. But maybe he is a new janitor and it’s his first day. Then he will find out that Alice is just messing with him only after starting running the errand.

But how does this relate to web development?

If we treat a (POST) web request as a command, an intent for a change, all these things can happen. Alice is the user, while Bob is our application. The request can be malformed, it might not make sense in an obvious way or it might come from someone that does not have privileges to give such commands. This is a class of errors that are relatively easy to detect upon receiving the command and before acknowledging it. As such, it makes sense to validate these things immediately after receiving the command (HTTP request). This is what a validation in the controller (or any other kind of a web layer) is.

However it might also happen that we start to perform some action. We fetched an order from a database and were told to add an item to it. But we can see it’s already shipped, so we cannot change it. This is the second kind - not really a validation, but a check failure during execution. Or, as we might call it, a violation of a business invariant.

What’s the difference? First of all, these checks lie in the domain code, not in a web layer. Secondly, you should not expect all of these failures to be returned at once. If Bob did not find the broom in the garage, he did not go into the kitchen to find that the floor is lava. If the user is banned from adding items to the order, the check for the already-shipped-order would not be performed.

Unfortunately, many practices encouraged by frameworks taught us to try to reduce the whole validation to one layer. The check if the email address was correctly formatted to be equal to the check if the user did not make too many purchases with delayed payment this month. The reality is that these are different things and as such should be modeled differently in different places.

Suggestions

There is no code in this article on purpose. However I want to end it with some more practical tips.

  • Use names “input validation” and “domain checks” instead of too generic “validation”. Things will start to fall into places.
  • Don’t think too much about returning all the errors at once. You should model a process, not a transaction, and processes stop at the first obstacle.
  • Remember about the grey area! It’s sometimes not obvious to which layer the validation should go. Use your best judgment. But just because the rules are not crystal clear does not mean that the whole idea is wrong and you should squash all the validations in one place.

As for technology choices in some frameworks I mostly write about.

  • Consider using separate technologies for both checks. In Rails world you could use dry-validation for input validations and ActiveRecord validation for domain checks. Another approach would be to heavily use form objects (input validation) and limit model validations to actual business invariants.
  • In Hanami that would be a difference between built-in params validation and something inside an operation or other domain contruct
  • In Phoenix/Ecto it might be tempting to go with changesets for both, but there are libs like Drops that are perfect for input validations.
  • But all in all, while you probably should use some library for the input validations, you might want to completely drop it for domain checks. Using exceptions in Ruby or with statements in Elixir are actually great for it.
end of the article

Tags: ruby elixir general thoughts

This article was written by me – Paweł Świątkowski – on 05 Feb 2025. I'm on Fediverse (Ruby-flavoured account, Elixir-flavoured account). Let's talk.