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?
Services hold business logic so it can be reused and tested independently of HTTP and database details.
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.