Skip to Content
Course content

206: Data Validation Pipelines with Scala

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this play out in a dozen different code reviews: a developer builds a registration form or an API endpoint, and the validation logic looks like a giant nesting doll of if/else blocks or a sequence of for-comprehensions using Option or Either. On the surface, it looks clean. It's typesafe, it handles errors, and it compiles. But the moment a user interacts with it, the experience is miserable.

The Whack-a-Mole Experience

Let's say we're validating a UserRegistration request. We need to check if the email is valid, if the password is strong enough, and if the age is above 18. The "naive" functional approach usually looks something like this:

case class UserRegistration(email: String, password: String, age: Int)

def validateEmail(email: String): Either[String, String] = 
  if (email.contains("@")) Right(email) else Left("Invalid email format")

def validatePassword(pw: String): Either[String, String] = 
  if (pw.length >= 8) Right(pw) else Left("Password too short")

def validateAge(age: Int): Either[String, Int] = 
  if (age >= 18) Right(age) else Left("Must be 18 or older")

def validateUser(reg: UserRegistration) = for {
  email <- validateEmail(reg.email)
  pw    <- validatePassword(reg.password)
  age   <- validateAge(reg.age)
} yield User(email, pw, age)

The problem here is that Either is designed to short-circuit. It's a "fail-fast" mechanism. If the email is invalid, the code stops right there and returns the email error. The user fixes their email, hits submit again, and then the system tells them their password is too short. They fix the password, submit again, and then it tells them they're too young. It's a game of whack-a-mole that drives users crazy.

Accumulating Failures instead of Short-Circuiting

In a real-world pipeline, you don't want the first error; you want all the errors. To do this, we have to move away from the monadic behavior of Either and toward an applicative style. In the Scala ecosystem, this is where Validated (from the Cats library) usually comes in, but the mental model is what matters most: we want to collect a list of failures rather than stopping at the first one.

Instead of a single String for an error, we use a NonEmptyList (Nel). This ensures that if we are in the "Failure" state, we definitely have at least one error to report. I prefer this because it removes the possibility of having an "Error" state that contains an empty list of reasons, which would be a logic bug.

import cats.data.ValidatedNel
import cats.implicits._

type ValidationResult[A] = ValidatedNel[String, A]

def validateEmail(email: String): ValidationResult[String] = 
  if (email.contains("@")) email.validNel else "Invalid email format".invalidNel

def validatePassword(pw: String): ValidationResult[String] = 
  if (pw.length >= 8) pw.validNel else "Password too short".invalidNel

def validateAge(age: Int): ValidationResult[Int] = 
  if (age >= 18) age.validNel else "Must be 18 or older".invalidNel

def validateUser(reg: UserRegistration): ValidationResult[User] = {
  (
    validateEmail(reg.email),
    validatePassword(reg.password),
    validateAge(reg.age)
  ).mapN(User.apply) 
}

The Trade-off: Cognitive Load vs. User Experience

Now, let's be honest: mapN and ValidatedNel are more "noisy" than a simple for-comprehension. You're introducing a different set of type classes and a slightly steeper learning curve for anyone joining your project. If you're writing a small internal CLI tool where you're the only user, the short-circuiting Either is perfectly fine—it's faster to write and easier to read.

But for any public-facing API or UI, the cost of the extra complexity is worth it. By treating validation as a parallel accumulation of results rather than a sequential chain of dependencies, you transform the API from a frustrating bottleneck into a helpful guide. You're telling the user everything they need to fix in one go, which is the hallmark of professional software.




📋 Practical Task

Building a Robust Order Validation Pipeline

You are building the checkout logic for an e-commerce platform. An Order is only valid if it meets the following criteria:

  • The orderId must not be empty.
  • The itemCount must be greater than 0.
  • The shippingAddress must contain at least one comma (representing city/state separation).
  • The totalAmount must be positive.
Your Task:
  1. Define a case class OrderRequest with the fields mentioned above.
  2. Implement four separate validation functions that return ValidatedNel[String, A].
  3. Create a validateOrder function that uses mapN to combine these validations into a final Order case class.
  4. Test your pipeline with an OrderRequest that has three invalid fields and print the resulting list of errors to the console to verify that no errors were dropped.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.