Validation & Exception Handling

Why Validation Matters

18 min Lesson 1 of 13

Why Validation Matters

Every application that accepts input from the outside world faces an uncomfortable truth: you cannot trust anything you did not create yourself. HTTP request bodies, query parameters, uploaded files, webhook payloads, messages from a queue — all of it arrives unverified. A user might mistype a value, a client might have a bug, or a malicious actor might deliberately craft data designed to corrupt your database, trigger unexpected behaviour, or breach your security perimeter.

Validation is the gate that stands between the chaotic outside world and the ordered interior of your application. In this tutorial you will learn to build that gate systematically — using the standard Java/Jakarta Bean Validation API, Spring Boot's integration of it, and the patterns that make a production API both robust and honest about what it rejects and why.

The Cost of Skipping Validation

It is tempting to assume the client will always send well-formed data, especially when you control both ends. Resist that assumption. The actual costs of missing validation are:

  • Data corruption: A null email address or a negative price stored in the database creates inconsistencies that are expensive to clean up and may be irreversible.
  • Unexpected exceptions: A service that receives a null where it expects a string will throw a NullPointerException deep inside business logic, producing a useless 500 response and a confusing stack trace.
  • Security vulnerabilities: Unvalidated string fields can carry SQL injection fragments, script tags for XSS attacks, or path-traversal sequences (../../etc/passwd) if the value is used in a file path.
  • Violated invariants: Your domain model has rules — an order total must be positive, a username must be unique, a date range must start before it ends. If you skip validation, those invariants leak into every downstream method, making the code fragile everywhere instead of defended at the edge.
Client-side validation is UI polish, not security. Any JavaScript validation in a browser can be bypassed in seconds with a tool like curl or Postman. Server-side validation is the only validation that counts for correctness and security. Client-side helps UX; server-side is mandatory.

Where Validation Belongs in a Spring Boot Application

A Spring Boot application typically has several layers — controller, service, repository. Validation can in principle happen at any of them, but the question of where it should happen depends on what you are validating.

  • Controller / API boundary: This is the right place to validate the shape and format of incoming data — is the required field present? Is the email a valid email address? Is the integer within an acceptable range? This kind of validation is fast, stateless, and can be expressed declaratively with annotations. Catching bad input here avoids wasting any downstream work.
  • Service layer: This is the right place to validate business rules that require application context — is this username already taken? Does the referenced product actually exist? Does the discount exceed the configured maximum? These checks often need a database query or a call to another service; they cannot be expressed as simple annotations.
  • Repository / database: Database constraints (NOT NULL, UNIQUE, foreign keys, check constraints) are the last line of defence. They protect data integrity even if application code has a bug. But they should complement application validation, not replace it — a constraint violation from the database produces a cryptic JDBC exception, not a friendly error message for the API client.
The layered defence principle: Good applications validate at every meaningful boundary — API format checks at the controller, business rule checks in the service, and hard integrity constraints in the database. Each layer catches the errors that belong to it; none tries to do the job of another.

A Concrete Example: What Can Go Wrong

Consider a simple POST /api/users endpoint that creates a new user. Without any validation the controller might look like this:

// BAD — no validation at all @PostMapping("/api/users") public ResponseEntity<UserResponse> createUser(@RequestBody UserRequest request) { User user = userService.createUser( request.getUsername(), request.getEmail(), request.getAge() ); return ResponseEntity.status(HttpStatus.CREATED).body(new UserResponse(user)); }

Now imagine a client sends this JSON body:

{ "username": "", "email": "not-an-email", "age": -5 }

Without validation, this request travels all the way into the service layer. The service tries to persist a user with a blank username, a malformed email, and a negative age. Depending on your database schema you will get either a corrupted row or an opaque SQL constraint violation that turns into an unhelpful 500 response — the client gets no information about what it did wrong.

A properly validated endpoint would instead return an immediate 400 Bad Request response listing exactly which fields are invalid and why, without touching the database at all:

// Response: 400 Bad Request { "status": 400, "error": "Validation Failed", "violations": [ { "field": "username", "message": "must not be blank" }, { "field": "email", "message": "must be a valid email address" }, { "field": "age", "message": "must be greater than 0" } ] }

This is the behaviour that professional APIs produce, and it is what you will build over this tutorial.

Jakarta Bean Validation: The Standard

Java has a formal specification for declarative validation: Jakarta Bean Validation 3.0 (formerly javax.validation, now jakarta.validation). The specification defines annotations like @NotNull, @Size, @Email, and @Min that you place directly on the fields of your Java beans. A validator — the reference implementation is Hibernate Validator — reads those annotations at runtime and evaluates them against actual values.

Spring Boot 3 includes the spring-boot-starter-validation starter, which pulls in Hibernate Validator automatically. You declare what valid data looks like; Spring takes care of running the checks and turning failures into proper HTTP responses:

<!-- pom.xml --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
Always add spring-boot-starter-validation to new Spring Boot projects. It is not included in spring-boot-starter-web by default. Forgetting it means your @Valid annotations silently do nothing — a common and confusing mistake.

What This Tutorial Covers

Now that you understand the motivation and the landscape, here is the path ahead:

  1. Applying Bean Validation annotations to request DTOs (@NotBlank, @Email, @Min, @Pattern, and more).
  2. Triggering validation in controllers with @Valid and @Validated.
  3. Writing your own constraint annotations when the built-in ones are not enough.
  4. Transforming validation failures into consistent, informative error responses.
  5. Handling unexpected exceptions globally with @ControllerAdvice.
  6. Advanced scenarios: validation groups, cross-field constraints, and service-layer validation.

By the end you will have a reusable validation and error-handling foundation that you can drop into any Spring Boot 3 project.

Summary

Never trust external input — validate it at the earliest appropriate boundary. Format and structural checks belong at the API layer (controller); business rule checks belong in the service layer; database constraints are a safety net, not a substitute. Spring Boot 3 provides first-class support for Jakarta Bean Validation through spring-boot-starter-validation. The rest of this tutorial shows you how to use it well.