Custom Exceptions & Service-Layer Signaling
Master domain-specific custom exceptions, unchecked exception design, Optional.orElseThrow() business signals, and clean controller decoupling.
Custom Exceptions & Service-Layer Signaling
To maintain clean separation of concerns, Controllers should focus exclusively on request dispatching and success responses, while Services signal business failures using domain-specific unchecked exceptions.
1. Why Error Handling Should Not Live Inside Controllers
Consider an anti-pattern common in beginner applications:
// ANTI-PATTERN: Polluted Controller method with inline null checks and error building
@GetMapping("/{id}")
public ResponseEntity<?> getStudentById(@PathVariable Long id) {
Student student = studentService.getStudentById(id);
if (student == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Student not found");
}
return ResponseEntity.ok(student);
}Problems with Inline Controller Error Handling:
- Code Duplication: Every endpoint duplicates
if (x == null)checks and status building. - Ambiguous Signals: Returning
nullfrom services is ambiguous (Does it mean missing data, DB error, or bug?). - Bloated Controllers: Controllers become mini-service layers responsible for error translation.
2. Domain-Specific Custom Exceptions
Instead of generic Java exceptions (RuntimeException or IllegalArgumentException), define domain-specific exceptions extending RuntimeException:
// 1. Missing Resource Exception
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
// 2. Duplicate Resource Conflict Exception
public class DuplicateResourceException extends RuntimeException {
public DuplicateResourceException(String message) {
super(message);
}
}Why
RuntimeException(Unchecked Exception)? ExtendingRuntimeExceptioneliminates mandatorythrowsclauses across method signatures in the Service and Repository layers, allowing exceptions to bubble up cleanly to global exception handlers.
3. Optional.orElseThrow() as a Business Signal
Replace ambiguous null returns in the Service layer with explicit Optional.orElseThrow() signals:
@Service
public class StudentService {
private final StudentRepository repository;
public StudentService(StudentRepository repository) {
this.repository = repository;
}
public StudentResponseDto getStudentById(Long id) {
// Clean business signal: Return Entity if present, or throw ResourceNotFoundException
Student student = repository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Student with id " + id + " not found"));
return mapToResponseDto(student);
}
public StudentResponseDto createStudent(StudentRequestDto request) {
// Business Rule: Email uniqueness check
if (repository.existsByEmail(request.getEmail())) {
throw new DuplicateResourceException("Student with email " + request.getEmail() + " already exists");
}
Student student = mapToEntity(request);
return mapToResponseDto(repository.save(student));
}
}4. Clean Controller Architecture
With custom exceptions and global exception handling, Controllers contain ZERO error-handling code:
@RestController
@RequestMapping("/api/students")
public class StudentController {
private final StudentService service;
public StudentController(StudentService service) {
this.service = service;
}
@GetMapping("/{id}")
public ResponseEntity<StudentResponseDto> getStudentById(@PathVariable Long id) {
// Only handles the happy path! If student is missing, service throws ResourceNotFoundException
StudentResponseDto student = service.getStudentById(id);
return ResponseEntity.ok(student);
}
}❓ Knowledge Check
Why do domain custom exceptions in Spring Boot extend RuntimeException instead of Exception?
What is the advantage of using repository.findById(id).orElseThrow(...) over returning null?
HTTP Response Anatomy & ResponseEntity
Master HTTP response structure, status code taxonomy (200, 201, 204, 400, 404, 409, 500), decision matrices, and ResponseEntity construction.
@RestControllerAdvice & Exception Mappings
Master centralized exception handling with @RestControllerAdvice, @ExceptionHandler, ErrorResponseDto envelopes, and HTTP 400 vs 409 semantics.