💻 Part 3: Implementation – Controller vs Service Patterns
🧭 Series Navigation:
- 🏠 Exception Handling Hub
- ▶️ Part 1: Foundation
- ▶️ Part 2: Architecture
- ✅ Part 3: Implementation (Current)
- ▶️ Part 4: Production Guide
💻 Implementation Patterns
Mastering Controller vs Service Layer Exception Handling
🎯 What You’ll Master
- Decision Framework: When to use exceptions vs normal flow
- Controller Patterns: HTTP-specific exception handling
- Service Patterns: Business logic exception management
- Anti-Patterns: Common mistakes to avoid
🤔 The Critical Decision: Exception vs Normal Flow
The most important decision in exception handling is when to use exceptions versus normal control flow. This determines your system’s performance, maintainability, and clarity.
✅ Use Exceptions For
- Unexpected failures (database down, network timeout)
- Business rule violations (duplicate email, insufficient balance)
- Security issues (unauthorized access, invalid tokens)
- System errors (configuration missing, service unavailable)
❌ Don’t Use Exceptions For
- Expected conditions (user not found in search)
- Control flow (pagination, filtering results)
- Optional operations (cache miss, feature flags)
- Performance-critical paths (hot loops, frequent calls)
🎮 Controller Layer Patterns
Controllers should handle HTTP-specific concerns and delegate business logic to services. Exception handling at this layer focuses on request/response transformation.
✅ Good Controller Pattern
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
@PostMapping
public ResponseEntity<ServiceResponse<User>> createUser(@Valid @RequestBody CreateUserRequest request) {
// Controller only handles HTTP concerns
User user = userService.createUser(request);
return ResponseEntity
.status(HttpStatus.CREATED)
.body(ServiceResponse.success("User created successfully", user));
}
@GetMapping("/{id}")
public ResponseEntity<ServiceResponse<User>> getUser(@PathVariable Long id) {
// Let GlobalExceptionHandler handle UserNotFoundException
User user = userService.findById(id);
return ResponseEntity.ok(ServiceResponse.success(user));
}
@GetMapping("/search")
public ResponseEntity<ServiceResponse<List<User>>> searchUsers(
@RequestParam String query,
@RequestParam(defaultValue = "0") int page) {
// Expected empty results - use normal flow, not exceptions
List<User> users = userService.searchUsers(query, page);
if (users.isEmpty()) {
return ResponseEntity.ok(ServiceResponse.success("No users found", users));
}
return ResponseEntity.ok(ServiceResponse.success(users));
}
}
❌ Anti-Pattern: Controller Exception Handling
@RestController
public class BadUserController {
@PostMapping("/users")
public ResponseEntity<?> createUser(@RequestBody CreateUserRequest request) {
try {
// DON'T: Handle business exceptions in controller
if (request.getEmail() == null) {
throw new ValidationException("Email is required");
}
if (userService.emailExists(request.getEmail())) {
throw new DuplicateException("Email already exists");
}
User user = userService.createUser(request);
return ResponseEntity.ok(user);
} catch (ValidationException e) {
// DON'T: Manual exception handling in controller
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
} catch (DuplicateException e) {
return ResponseEntity.status(409).body(Map.of("error", e.getMessage()));
} catch (Exception e) {
return ResponseEntity.status(500).body(Map.of("error", "Internal error"));
}
}
}
⚙️ Service Layer Patterns
Services handle business logic and should throw exceptions for business rule violations and system failures. This is where the priority order from Part 1 is crucial.
✅ Good Service Pattern
@Service
@Transactional
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
private final DatabaseHealthCheck healthCheck;
public User createUser(CreateUserRequest request) {
// 1. DEPENDENCY CHECK FIRST (5xx)
if (!healthCheck.isDatabaseAvailable()) {
throw new ServiceException(EXTERNAL_SERVICE_ERROR);
}
if (!emailService.isAvailable()) {
throw new ServiceException(EMAIL_SERVICE_UNAVAILABLE);
}
// 2. BUSINESS VALIDATION SECOND (4xx)
validateUserRequest(request);
if (userRepository.existsByEmail(request.getEmail())) {
throw new DuplicateException(USER_EMAIL_DUPLICATE, request.getEmail());
}
// 3. IMPLEMENTATION (5xx if fails)
try {
User user = User.builder()
.email(request.getEmail())
.name(request.getName())
.createdAt(LocalDateTime.now())
.build();
User savedUser = userRepository.save(user);
// Non-critical operation - don't fail the main flow
emailService.sendWelcomeEmailAsync(savedUser.getEmail());
return savedUser;
} catch (DataAccessException ex) {
log.error("Database error creating user: {}", ex.getMessage());
throw new ServiceException(MEMBER_REGISTRATION_FAILED, ex);
}
}
public User findById(Long id) {
// Business operation - throw exception if not found
return userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(USER_NOT_FOUND, id.toString()));
}
public List<User> searchUsers(String query, int page) {
// Expected operation - return empty list, don't throw exception
return userRepository.findByNameContainingIgnoreCase(
query,
PageRequest.of(page, 20)
).getContent();
}
private void validateUserRequest(CreateUserRequest request) {
if (request.getEmail() == null || request.getEmail().trim().isEmpty()) {
throw new ValidationException(INVALID_INPUT, "email", "null/empty");
}
if (!EmailValidator.isValid(request.getEmail())) {
throw new ValidationException(INVALID_EMAIL_FORMAT, "email", request.getEmail());
}
if (request.getName() == null || request.getName().trim().length() < 2) {
throw new ValidationException(INVALID_INPUT, "name", "too short");
}
}
}
🚫 Common Anti-Patterns to Avoid
❌ Exception for Control Flow
// DON'T: Use exceptions for expected conditions
public User findUserByEmail(String email) {
try {
return userRepository.findByEmail(email);
} catch (NoResultException e) {
throw new UserNotFoundException("User not found");
}
}
// DO: Use Optional for expected absence
public Optional<User> findUserByEmail(String email) {
return userRepository.findByEmail(email);
}
❌ Swallowing Exceptions
// DON'T: Silently catch and ignore
public void updateUser(User user) {
try {
userRepository.save(user);
} catch (Exception e) {
// Silent failure - very dangerous!
}
}
// DO: Handle appropriately or let it propagate
public void updateUser(User user) {
try {
userRepository.save(user);
} catch (DataAccessException e) {
log.error("Failed to update user {}: {}", user.getId(), e.getMessage());
throw new ServiceException(USER_UPDATE_FAILED, e);
}
}
❌ Generic Exception Types
// DON'T: Throw generic exceptions
public void validateUser(User user) {
if (user.getEmail() == null) {
throw new RuntimeException("Email is required");
}
}
// DO: Use specific, typed exceptions
public void validateUser(User user) {
if (user.getEmail() == null) {
throw new ValidationException(INVALID_INPUT, "email", "null");
}
}
🎯 Decision Framework
Use this framework to decide when and where to handle exceptions:
🤔 Decision Questions
- Is this condition expected in normal operation?
Expected → Normal flow | Unexpected → Exception - Can the caller reasonably handle this condition?
Yes → Specific exception | No → Generic system exception - Is this a business rule violation or system failure?
Business → 4xx exception | System → 5xx exception - Does this affect the core operation’s success?
Core → Exception | Side effect → Log and continue
✅ Implementation Best Practices
🎮 Controller Layer
- Keep it thin – delegate to services
- Handle only HTTP-specific concerns
- Let GlobalExceptionHandler manage exceptions
- Use @Valid for request validation
⚙️ Service Layer
- Follow dependency → business → implementation order
- Use specific exception types
- Include context in exception messages
- Log appropriately by severity
📊 Performance
- Exceptions are expensive – use wisely
- Cache validation results when possible
- Fail fast on dependency checks
- Use async for non-critical operations
🎯 Part 3 Mastery Achieved
✅ Decision Framework
- Exception vs normal flow criteria
- Layer-specific responsibilities
- Performance considerations
✅ Controller Patterns
- Thin controller principle
- HTTP-specific error handling
- Delegation to services
✅ Service Patterns
- Priority order implementation
- Business logic exception handling
- Specific exception types
✅ Anti-Pattern Awareness
- Common mistakes to avoid
- Performance implications
- Maintainability concerns
🔗 Continue Your Journey
🧭 Series Navigation:
- 🏠 Exception Handling Hub
- ▶️ Part 1: Foundation
- ▶️ Part 2: Architecture
- ✅ Part 3: Implementation (Current)
- ▶️ Part 4: Production Guide