Spring Boot · Lesson 5 of 7

Services, validation & error handling

Layered architecture, bean validation, @Transactional and global exception handling.

Real apps use three layers: controller (HTTP), service (business rules), repository (database). Controllers stay thin; logic lives in services.

Validation

Add the Validation starter, put constraints such as @NotBlank, @Email and @Positive on your request DTO, and mark the parameter @Valid. Invalid requests are rejected before your code runs.

Error handling

A @RestControllerAdvice class with @ExceptionHandler methods turns exceptions into clean JSON responses with the right status — in one place, for all controllers.

@Transactional on a service method makes its database changes all-or-nothing.

Example code

Spring Boot 3 · Java 17+
public record CreateJobRequest(
    @NotBlank String title,
    @NotBlank String city,
    @Positive double salaryLpa) {}

public class JobNotFoundException extends RuntimeException {
    public JobNotFoundException(Long id) { super("Job " + id + " not found"); }
}

@Service
public class JobService {
    private final JobRepository repo;
    public JobService(JobRepository repo) { this.repo = repo; }

    @Transactional
    public Job create(CreateJobRequest req) {
        return repo.save(new Job(req.title(), req.city(), req.salaryLpa()));
    }

    public Job get(Long id) {
        return repo.findById(id).orElseThrow(() -> new JobNotFoundException(id));
    }
}

@RestControllerAdvice
public class ApiErrors {
    @ExceptionHandler(JobNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public Map<String, String> notFound(JobNotFoundException e) {
        return Map.of("error", e.getMessage());
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Map<String, String> invalid(MethodArgumentNotValidException e) {
        Map<String, String> errors = new HashMap<>();
        e.getBindingResult().getFieldErrors()
         .forEach(f -> errors.put(f.getField(), f.getDefaultMessage()));
        return errors;
    }
}

// controller: public Job create(@Valid @RequestBody CreateJobRequest req)

Output / API response

POST /api/jobs  {"title":"","city":"Kochi","salaryLpa":-2}
→ 400 Bad Request
  {"title":"must not be blank","salaryLpa":"must be greater than 0"}

GET /api/jobs/99
→ 404 Not Found
  {"error":"Job 99 not found"}

Generate a project at start.spring.io (Maven, Java 17+, dependencies: Spring Web, Spring Data JPA, H2, Validation), add the code, and run ./mvnw spring-boot:run. Learn the Java tutorial first if you're new to Java. Data is fictional.

Key points

  • Controller → service → repository keeps code organised.
  • Validate request DTOs with annotations and @Valid.
  • Handle exceptions once with @RestControllerAdvice.
  • @Transactional makes a set of changes all-or-nothing.

Quick check

Where should business rules usually live?

Practice task

Add a rule in JobService that a job title must be unique (throw a DuplicateJobException), and map it to 409 Conflict in ApiErrors.

Interview corner

How do you handle exceptions globally in Spring Boot?

Create a class annotated with @RestControllerAdvice containing @ExceptionHandler methods for each exception type. They return a consistent error body and status code, so controllers don't need try-catch blocks.

When does @Transactional not work?

When the method is called from another method in the same class (the call skips Spring's proxy), when it's on a private method, or when a checked exception is thrown (by default only runtime exceptions trigger rollback).

Further reading

Official documentation, linked for deeper study. These are outside sites; this lesson's text is our own.

Original lesson written by the InfoparkDaily team · © 2026 InfoparkDaily · Free to learn — no paid course or subscription.