Spring Framework Patterns
Purpose
This skill provides comprehensive patterns and best practices for Spring Framework and Spring Boot development. It serves as a reference guide during code reviews to ensure Spring applications follow industry standards, are maintainable, scalable, and adhere to enterprise Java conventions.
When to use this skill:
- Conducting code reviews of Spring/Spring Boot applications
- Designing Spring Boot application architecture
- Writing new Spring components (controllers, services, repositories)
- Refactoring existing Spring applications
- Evaluating Spring configuration and setup
- Teaching Spring best practices to team members
Context
Spring Framework is the de facto standard for enterprise Java applications. This skill documents production-ready patterns using Spring Boot 3.x+ and Spring 6.x+, emphasizing:
- Modularity: Clear separation of concerns with proper layering
- Maintainability: Code that's easy to understand and modify
- Testability: Components that can be easily tested
- Performance: Efficient use of Spring features
- Security: Secure-by-default patterns
- Convention over Configuration: Leveraging Spring Boot auto-configuration
This skill is designed to be referenced by the uncle-duke-java agent during code reviews and by developers when implementing Spring applications.
Prerequisites
Required Knowledge:
- Java fundamentals (Java 17+)
- Object-oriented programming concepts
- Basic understanding of Spring concepts
- Maven or Gradle basics
Required Tools:
- JDK 17 or later
- Spring Boot 3.x+
- Maven or Gradle
- IDE (IntelliJ IDEA, Eclipse, VS Code)
Expected Project Structure:
spring-boot-app/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/example/app/
│ │ │ ├── Application.java
│ │ │ ├── config/
│ │ │ ├── controller/
│ │ │ ├── service/
│ │ │ ├── repository/
│ │ │ ├── model/
│ │ │ ├── dto/
│ │ │ ├── exception/
│ │ │ └── security/
│ │ └── resources/
│ │ ├── application.yml
│ │ ├── application-dev.yml
│ │ ├── application-prod.yml
│ │ └── db/migration/
│ └── test/
│ └── java/
├── pom.xml (or build.gradle)
└── README.md
Instructions
Task 1: Implement Dependency Injection Best Practices
1.1 Constructor Injection (Preferred)
Rule: ALWAYS use constructor injection for required dependencies. Never use field injection in production code.
✅ Good:
@Service
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
// Constructor injection - preferred approach
public UserService(UserRepository userRepository, EmailService emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
public User createUser(UserDTO dto) {
User user = new User(dto.getEmail());
User savedUser = userRepository.save(user);
emailService.sendWelcomeEmail(savedUser);
return savedUser;
}
}
Why good:
- Dependencies are immutable (final fields)
- Dependencies are mandatory - cannot create instance without them
- Easy to test - can inject mocks in tests
- No reflection needed in tests
- Constructor clearly documents all dependencies
❌ Bad:
@Service
public class UserService {
// Field injection - DON'T DO THIS
@Autowired
private UserRepository userRepository;
@Autowired
private EmailService emailService;
public User createUser(UserDTO dto) {
User user = new User(dto.getEmail());
User savedUser = userRepository.save(user);
emailService.sendWelcomeEmail(savedUser);
return savedUser;
}
}
Why bad:
- Cannot be final - mutable dependencies
- Can create instance without dependencies (NullPointerException risk)
- Hard to test - requires reflection or Spring context in tests
- Hides dependencies - not clear what's required
- Violates encapsulation
With Lombok (Acceptable):
@Service
@RequiredArgsConstructor // Generates constructor for final fields
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
public User createUser(UserDTO dto) {
User user = new User(dto.getEmail());
User savedUser = userRepository.save(user);
emailService.sendWelcomeEmail(savedUser);
return savedUser;
}
}
1.2 Optional Dependencies with Setter Injection
Rule: Use setter injection ONLY for optional dependencies.
✅ Good:
@Service
public class NotificationService {
private final EmailService emailService; // Required
private SmsService smsService; // Optional
public NotificationService(EmailService emailService) {
this.emailService = emailService;
}
@Autowired(required = false)
public void setSmsService(SmsService smsService) {
this.smsService = smsService;
}
public void notify(User user, String message) {
emailService.send(user.getEmail(), message);
if (smsService != null && user.getPhoneNumber() != null) {
smsService.send(user.getPhoneNumber(), message);
}
}
}
1.3 Avoiding Circular Dependencies
Rule: Circular dependencies indicate design problems. Refactor instead of using @Lazy.
❌ Bad:
@Service
public class OrderService {
@Autowired
@Lazy // Band-aid solution
private PaymentService paymentService;
}
@Service
public class PaymentService {
@Autowired
@Lazy // Band-aid solution
private OrderService orderService;
}
✅ Good:
// Extract common logic to a new service
@Service
public class OrderProcessingService {
private final OrderRepository orderRepository;
private final PaymentRepository paymentRepository;
public OrderProcessingService(OrderRepository orderRepository,
PaymentRepository paymentRepository) {
this.orderRepository = orderRepository;
this.paymentRepository = paymentRepository;
}
public void processOrder(Order order, Payment payment) {
order.setStatus(OrderStatus.PROCESSING);
orderRepository.save(order);
payment.setOrderId(order.getId());
paymentRepository.save(payment);
}
}
@Service
public class OrderService {
private final OrderProcessingService processingService;
// ...
}
@Service
public class PaymentService {
private final OrderProcessingService processingService;
// ...
}
Task 2: Understand Bean Lifecycle and Scopes
2.1 Bean Scopes
Available Scopes:
singleton(default): One instance per Spring containerprototype: New instance each time bean is requestedrequest: One instance per HTTP request (web applications)session: One instance per HTTP session (web applications)application: One instance per ServletContext (web applications)
✅ Good:
// Singleton (default) - stateless services
@Service
@Scope("singleton") // Can omit - it's default
public class UserService {
// Stateless - safe to share
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
// Prototype - stateful beans
@Component
@Scope("prototype")
public class ReportGenerator {
// Stateful - each user gets their own instance
private final List<String> reportLines = new ArrayList<>();
public void addLine(String line) {
reportLines.add(line);
}
public String generate() {
return String.join("\n", reportLines);
}
}
// Request scope - web layer
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext {
private String requestId;
private String userId;
// Getters and setters
}
❌ Bad:
// Singleton with mutable state - THREAD UNSAFE
@Service
public class UserService {
private User currentUser; // Shared across all requests - BAD!
public void processUser(User user) {
this.currentUser = user; // Race condition!
// Process...
}
}
2.2 Bean Lifecycle Callbacks
Rule: Use @PostConstruct for initialization, @PreDestroy for cleanup.
✅ Good:
@Service
public class CacheService {
private final Map<String, Object> cache = new ConcurrentHashMap<>();
private ScheduledExecutorService scheduler;
@PostConstruct
public void initialize() {
System.out.println("Initializing cache service...");
scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(this::cleanupExpiredEntries, 1, 1, TimeUnit.HOURS);
}
@PreDestroy
public void cleanup() {
System.out.println("Cleaning up cache service...");
cache.clear();
if (scheduler != null) {
scheduler.shutdown();
}
}
private void cleanupExpiredEntries() {
// Cleanup logic
}
}
2.3 Component Stereotypes
Rule: Use the most specific stereotype annotation.
@Component // Generic Spring-managed component
@Service // Business logic layer
@Repository // Data access layer (adds exception translation)
@Controller // MVC controller (returns views)
@RestController // REST API controller (returns data)
@Configuration // Configuration class
✅ Good:
@Repository // Data access - enables exception translation
public interface UserRepository extends JpaRepository<User, Long> {
}
@Service // Business logic
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
@RestController // REST API
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
}
@Configuration // Configuration
public class SecurityConfig {
// Configuration beans
}
Task 3: Design REST API Controllers
3.1 Controller Structure
Rule: Keep controllers thin - delegate business logic to services.
✅ Good:
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping
public ResponseEntity<Page<UserDTO>> getAllUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "id") String sortBy) {
Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy));
Page<UserDTO> users = userService.findAll(pageable);
return ResponseEntity.ok(users);
}
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getUserById(@PathVariable Long id) {
return userService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<UserDTO> createUser(@Valid @RequestBody CreateUserRequest request) {
UserDTO created = userService.createUser(request);
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(created.getId())
.toUri();
return ResponseEntity.created(location).body(created);
}
@PutMapping("/{id}")
public ResponseEntity<UserDTO> updateUser(
@PathVariable Long id,
@Valid @RequestBody UpdateUserRequest request) {
return userService.updateUser(id, request)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
}
Why good:
- RESTful URL structure
- Proper HTTP methods and status codes
- Pagination and sorting support
- Validation with @Valid
- Location header for created resources
- Delegates logic to service layer
❌ Bad:
@RestController
public class UserController {
@Autowired
private UserRepository userRepository; // Controller accessing repository directly!
@GetMapping("/getUsers") // Non-RESTful URL
public List<User> getUsers() { // Returns entities, not DTOs
return userRepository.findAll(); // Business logic in controller
}
@PostMapping("/createUser") // Non-RESTful URL
public User createUser(@RequestBody User user) { // No validation
// Business logic in controller - BAD!
if (userRepository.findByEmail(user.getEmail()).isPresent()) {
throw new RuntimeException("Email exists"); // Poor error handling
}
return userRepository.save(user); // Returns 200 instead of 201
}
}
3.2 Request/Response Patterns
Rule: Use DTOs for API contracts. Never expose entities directly.
✅ Good:
// Request DTOs
public class CreateUserRequest {
@NotBlank(message = "Email is required")
@Email(message = "Email must be valid")
private String email;
@NotBlank(message = "Name is required")
@Size(min = 2, max = 100, message = "Name must be between 2 and 100 characters")
private String name;
@NotBlank(message = "Password is required")
@Size(min = 8, message = "Password must be at least 8 characters")
private String password;
// Getters and setters
}
// Response DTOs
public class UserDTO {
private Long id;
private String email;
private String name;
private LocalDateTime createdAt;
private boolean active;
// No password field - security
// Getters and setters
}
// Service layer
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final UserMapper userMapper;
public UserDTO createUser(CreateUserRequest request) {
// Validate
if (userRepository.existsByEmail(request.getEmail())) {
throw new EmailAlreadyExistsException(request.getEmail());
}
// Map DTO to entity
User user = new User();
user.setEmail(request.getEmail());
user.setName(request.getName());
user.setPassword(passwordEncoder.encode(request.getPassword()));
user.setActive(true);
user.setCreatedAt(LocalDateTime.now());
// Save
User saved = userRepository.save(user);
// Map entity to DTO
return userMapper.toDTO(saved);
}
}
3.3 HTTP Status Codes
Rule: Use correct HTTP status codes.
@RestController
@RequestMapping("/api/v1/orders")
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
// 200 OK - Successful GET/PUT
@GetMapping("/{id}")
public ResponseEntity<OrderDTO> getOrder(@PathVariable Long id) {
return orderService.findById(id)
.map(ResponseEntity::ok) // 200 OK
.orElse(ResponseEntity.notFound().build()); // 404 Not Found
}
// 201 Created - Successful POST
@PostMapping
public ResponseEntity<OrderDTO> createOrder(@Valid @RequestBody CreateOrderRequest request) {
OrderDTO created = orderService.createOrder(request);
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(created.getId())
.toUri();
return ResponseEntity.created(location).body(created); // 201 Created
}
// 204 No Content - Successful DELETE
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteOrder(@PathVariable Long id) {
orderService.deleteOrder(id);
return ResponseEntity.noContent().build(); // 204 No Content
}
// 202 Accepted - Async processing
@PostMapping("/{id}/process")
public ResponseEntity<Void> processOrder(@PathVariable Long id) {
orderService.processOrderAsync(id);
return ResponseEntity.accepted().build(); // 202 Accepted
}
// 400 Bad Request - Validation failures (handled by @Valid)
// 401 Unauthorized - Not authenticated (handled by Security)
// 403 Forbidden - Not authorized (handled by Security)
// 404 Not Found - Resource doesn't exist
// 409 Conflict - Business rule violation
// 500 Internal Server Error - Unexpected errors
}
Task 4: Implement Data Access Layer with Spring Data JPA
4.1 Repository Interfaces
Rule: Extend appropriate Spring Data interface based on needs.
✅ Good:
// Simple CRUD - JpaRepository
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// Query methods - Spring Data generates implementation
Optional<User> findByEmail(String email);
boolean existsByEmail(String email);
List<User> findByActiveTrue();
// Custom query
@Query("SELECT u FROM User u WHERE u.createdAt > :date")
List<User> findRecentUsers(@Param("date") LocalDateTime date);
// Native query
@Query(value = "SELECT * FROM users WHERE email LIKE %:domain", nativeQuery = true)
List<User> findByEmailDomain(@Param("domain") String domain);
// Modifying query
@Modifying
@Query("UPDATE User u SET u.active = false WHERE u.lastLoginAt < :date")
int deactivateInactiveUsers(@Param("date") LocalDateTime date);
}
Repository Hierarchy:
Repository<T, ID>- Marker interface, no methodsCrudRepository<T, ID>- Basic CRUD operationsPagingAndSortingRepository<T, ID>- Adds pagination and sortingJpaRepository<T, ID>- JPA-specific features (flush, batch operations)
4.2 Custom Queries with Specifications
Rule: Use Specifications for dynamic queries instead of building query strings.
✅ Good:
// Specification
public class UserSpecifications {
public static Specification<User> hasEmail(String email) {
return (root, query, cb) ->
email == null ? null : cb.equal(root.get("email"), email);
}
public static Specification<User> isActive() {
return (root, query, cb) -> cb.isTrue(root.get("active"));
}
public static Specification<User> createdAfter(LocalDateTime date) {
return (root, query, cb) ->
date == null ? null : cb.greaterThan(root.get("createdAt"), date);
}
public static Specification<User> nameLike(String name) {
return (root, query, cb) ->
name == null ? null : cb.like(cb.lower(root.get("name")), "%" + name.toLowerCase() + "%");
}
}
// Repository
@Repository
public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {
}
// Service
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
public Page<User> searchUsers(UserSearchCriteria criteria, Pageable pageable) {
Specification<User> spec = Specification.where(null);
if (criteria.getEmail() != null) {
spec = spec.and(UserSpecifications.hasEmail(criteria.getEmail()));
}
if (criteria.isActiveOnly()) {
spec = spec.and(UserSpecifications.isActive());
}
if (criteria.getCreatedAfter() != null) {
spec = spec.and(UserSpecifications.createdAfter(criteria.getCreatedAfter()));
}
if (criteria.getName() != null) {
spec = spec.and(UserSpecifications.nameLike(criteria.getName()));
}
return userRepository.findAll(spec, pageable);
}
}
4.3 Pagination and Sorting
Rule: Always support pagination for list endpoints.
✅ Good:
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping
public ResponseEntity<Page<UserDTO>> getUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "id,desc") String[] sort) {
// Parse sort parameters
List<Sort.Order> orders = Arrays.stream(sort)
.map(s -> {
String[] parts = s.split(",");
String property = parts[0];
Sort.Direction direction = parts.length > 1 && parts[1].equalsIgnoreCase("desc")
? Sort.Direction.DESC
: Sort.Direction.ASC;
return new Sort.Order(direction, property);
})
.toList();
Pageable pageable = PageRequest.of(page, size, Sort.by(orders));
Page<UserDTO> users = userService.findAll(pageable);
return ResponseEntity.ok(users);
}
}
4.4 Transaction Management
Rule: Use @Transactional on service methods, not repository methods.
✅ Good:
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentRepository paymentRepository;
private final EmailService emailService;
@Transactional
public OrderDTO createOrder(CreateOrderRequest request) {
// Create order
Order order = new Order();
order.setUserId(request.getUserId());
order.setStatus(OrderStatus.PENDING);
Order savedOrder = orderRepository.save(order);
// Create payment
Payment payment = new Payment();
payment.setOrderId(savedOrder.getId());
payment.setAmount(request.getAmount());
paymentRepository.save(payment);
// If email fails, transaction rolls back
emailService.sendOrderConfirmation(savedOrder);
return mapToDTO(savedOrder);
}
@Transactional(readOnly = true) // Optimization for read-only operations
public Optional<OrderDTO> findById(Long id) {
return orderRepository.findById(id)
.map(this::mapToDTO);
}
@Transactional(isolation = Isolation.SERIALIZABLE) // For critical operations
public void processPayment(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
// Process payment with highest isolation level
order.setStatus(OrderStatus.PAID);
orderRepository.save(order);
}
}
Transaction Propagation:
@Service
public class UserService {
@Transactional(propagation = Propagation.REQUIRED) // Default - join existing or create new
public void updateUser(User user) {
// Uses existing transaction or creates new
}
@Transactional(propagation = Propagation.REQUIRES_NEW) // Always create new transaction
public void auditLog(String action) {
// Independent transaction - commits even if parent rolls back
}
@Transactional(propagation = Propagation.MANDATORY) // Must have existing transaction
public void criticalOperation() {
// Throws exception if no transaction exists
}
}
Task 5: Design Service Layer
5.1 Service Boundaries
Rule: Services should represent business capabilities, not data access.
✅ Good:
// Good service boundaries
@Service
@RequiredArgsConstructor
public class UserRegistrationService {
private final UserRepository userRepository;
private final EmailService emailService;
private final PasswordEncoder passwordEncoder;
@Transactional
public UserDTO register(RegistrationRequest request) {
// Validate
validateEmailNotExists(request.getEmail());
// Create user
User user = createUser(request);
User saved = userRepository.save(user);
// Send welcome email
emailService.sendWelcomeEmail(saved.getEmail());
return mapToDTO(saved);
}
private void validateEmailNotExists(String email) {
if (userRepository.existsByEmail(email)) {
throw new EmailAlreadyExistsException(email);
}
}
private User createUser(RegistrationRequest request) {
User user = new User();
user.setEmail(request.getEmail());
user.setName(request.getName());
user.setPassword(passwordEncoder.encode(request.getPassword()));
return user;
}
}
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
@Transactional(readOnly = true)
public Optional<UserDTO> findById(Long id) {
return userRepository.findById(id).map(this::mapToDTO);
}
@Transactional(readOnly = true)
public Page<UserDTO> findAll(Pageable pageable) {
return userRepository.findAll(pageable).map(this::mapToDTO);
}
}
5.2 DTO Mapping
Rule: Keep entity-to-DTO mapping logic in one place.
✅ Good with MapStruct:
@Mapper(componentModel = "spring")
public interface UserMapper {
UserDTO toDTO(User user);
List<UserDTO> toDTOs(List<User> users);
@Mapping(target = "id", ignore = true)
@Mapping(target = "createdAt", ignore = true)
User toEntity(CreateUserRequest request);
}
// Usage in service
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final UserMapper userMapper;
public UserDTO createUser(CreateUserRequest request) {
User user = userMapper.toEntity(request);
User saved = userRepository.save(user);
return userMapper.toDTO(saved);
}
}
✅ Good without MapStruct:
@Service
public class UserService {
private final UserRepository userRepository;
private UserDTO mapToDTO(User user) {
UserDTO dto = new UserDTO();
dto.setId(user.getId());
dto.setEmail(user.getEmail());
dto.setName(user.getName());
dto.setCreatedAt(user.getCreatedAt());
dto.setActive(user.isActive());
return dto;
}
private User mapToEntity(CreateUserRequest request) {
User user = new User();
user.setEmail(request.getEmail());
user.setName(request.getName());
return user;
}
}
Task 6: Implement Global Exception Handling
6.1 Custom Exceptions
Rule: Create domain-specific exceptions for business errors.
✅ Good:
// Base exception
public abstract class BusinessException extends RuntimeException {
private final String errorCode;
public BusinessException(String message, String errorCode) {
super(message);
this.errorCode = errorCode;
}
public String getErrorCode() {
return errorCode;
}
}
// Specific exceptions
public class ResourceNotFoundException extends BusinessException {
public ResourceNotFoundException(String resourceName, Long id) {
super(String.format("%s with id %d not found", resourceName, id), "RESOURCE_NOT_FOUND");
}
}
public class EmailAlreadyExistsException extends BusinessException {
public EmailAlreadyExistsException(String email) {
super(String.format("Email %s already exists", email), "EMAIL_EXISTS");
}
}
public class InsufficientBalanceException extends BusinessException {
public InsufficientBalanceException(Long accountId) {
super(String.format("Insufficient balance in account %d", accountId), "INSUFFICIENT_BALANCE");
}
}
6.2 Global Exception Handler with @ControllerAdvice
Rule: Centralize exception handling in @ControllerAdvice.
✅ Good:
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleResourceNotFound(ResourceNotFoundException ex) {
log.warn("Resource not found: {}", ex.getMessage());
ErrorResponse error = ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(HttpStatus.NOT_FOUND.value())
.error("Not Found")
.message(ex.getMessage())
.errorCode(ex.getErrorCode())
.build();
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
@ExceptionHandler(EmailAlreadyExistsException.class)
public ResponseEntity<ErrorResponse> handleEmailAlreadyExists(EmailAlreadyExistsException ex) {
log.warn("Email already exists: {}", ex.getMessage());
ErrorResponse error = ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(HttpStatus.CONFLICT.value())
.error("Conflict")
.message(ex.getMessage())
.errorCode(ex.getErrorCode())
.build();
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ValidationErrorResponse> handleValidationErrors(MethodArgumentNotValidException ex) {
log.warn("Validation failed: {}", ex.getMessage());
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
ValidationErrorResponse response = ValidationErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(HttpStatus.BAD_REQUEST.value())
.error("Validation Failed")
.message("Invalid request parameters")
.fieldErrors(errors)
.build();
return ResponseEntity.badRequest().body(response);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) {
log.error("Unexpected error occurred", ex);
ErrorResponse error = ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(HttpStatus.INTERNAL_SERVER_ERROR.value())
.error("Internal Server Error")
.message("An unexpected error occurred")
.build();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}
// Error response DTOs
@Data
@Builder
public class ErrorResponse {
private LocalDateTime timestamp;
private int status;
private String error;
private String message;
private String errorCode;
}
@Data
@Builder
public class ValidationErrorResponse {
private LocalDateTime timestamp;
private int status;
private String error;
private String message;
private Map<String, String> fieldErrors;
}
Task 7: Implement Spring Security
7.1 Security Configuration
Rule: Use security configuration classes for centralized security setup.
✅ Good (Spring Security 6+):
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/public/**").permitAll()
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
.requestMatchers("/api/v1/users/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.httpBasic(Customizer.withDefaults())
.formLogin(form -> form.disable())
.logout(logout -> logout
.logoutUrl("/api/v1/auth/logout")
.logoutSuccessHandler((request, response, authentication) ->
response.setStatus(HttpServletResponse.SC_OK))
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
}
7.2 JWT Implementation
Rule: Implement JWT for stateless authentication in REST APIs.
✅ Good:
@Component
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration:3600000}") // 1 hour default
private long expiration;
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put("roles", userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.toList());
return Jwts.builder()
.setClaims(claims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public String getUsernameFromToken(String token) {
return getClaimsFromToken(token).getSubject();
}
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
return !isTokenExpired(token);
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
private Claims getClaimsFromToken(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
private boolean isTokenExpired(String token) {
Date expiration = getClaimsFromToken(token).getExpiration();
return expiration.before(new Date());
}
}
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider tokenProvider;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = getTokenFromRequest(request);
if (token != null && tokenProvider.validateToken(token)) {
String username = tokenProvider.getUsernameFromToken(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private String getTokenFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
7.3 Method Security
Rule: Use method security for fine-grained authorization.
✅ Good:
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
@PreAuthorize("hasRole('ADMIN')")
public List<UserDTO> findAll() {
return userRepository.findAll().stream()
.map(this::mapToDTO)
.toList();
}
@PreAuthorize("hasRole('USER') and #userId == authentication.principal.id")
public UserDTO findById(Long userId) {
return userRepository.findById(userId)
.map(this::mapToDTO)
.orElseThrow(() -> new ResourceNotFoundException("User", userId));
}
@PreAuthorize("hasAnyRole('ADMIN', 'MODERATOR')")
@PostAuthorize("returnObject.email == authentication.principal.username")
public UserDTO updateUser(Long id, UpdateUserRequest request) {
// Implementation
}
@Secured("ROLE_ADMIN")
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
}
Task 8: Implement Testing Strategies
8.1 Unit Testing Services
Rule: Test services in isolation with mocked dependencies.
✅ Good:
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@Mock
private PasswordEncoder passwordEncoder;
@Mock
private EmailService emailService;
@InjectMocks
private UserService userService;
@Test
void createUser_WithValidData_ShouldCreateUser() {
// Arrange
CreateUserRequest request = new CreateUserRequest();
request.setEmail("test@example.com");
request.setPassword("password123");
User user = new User();
user.setId(1L);
user.setEmail(request.getEmail());
when(userRepository.existsByEmail(request.getEmail())).thenReturn(false);
when(passwordEncoder.encode(request.getPassword())).thenReturn("encodedPassword");
when(userRepository.save(any(User.class))).thenReturn(user);
// Act
UserDTO result = userService.createUser(request);
// Assert
assertNotNull(result);
assertEquals(1L, result.getId());
assertEquals("test@example.com", result.getEmail());
verify(userRepository).existsByEmail(request.getEmail());
verify(passwordEncoder).encode(request.getPassword());
verify(userRepository).save(any(User.class));
verify(emailService).sendWelcomeEmail(any(User.class));
}
@Test
void createUser_WithExistingEmail_ShouldThrowException() {
// Arrange
CreateUserRequest request = new CreateUserRequest();
request.setEmail("existing@example.com");
when(userRepository.existsByEmail(request.getEmail())).thenReturn(true);
// Act & Assert
assertThrows(EmailAlreadyExistsException.class, () ->
userService.createUser(request));
verify(userRepository).existsByEmail(request.getEmail());
verify(userRepository, never()).save(any(User.class));
}
}
8.2 Integration Testing with @SpringBootTest
Rule: Use @SpringBootTest for integration tests that need full context.
✅ Good:
@SpringBootTest
@AutoConfigureMockMvc
@Transactional // Rollback after each test
class UserControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private UserRepository userRepository;
@Autowired
private ObjectMapper objectMapper;
@BeforeEach
void setUp() {
userRepository.deleteAll();
}
@Test
void createUser_WithValidData_ShouldReturn201() throws Exception {
// Arrange
CreateUserRequest request = new CreateUserRequest();
request.setEmail("test@example.com");
request.setName("Test User");
request.setPassword("password123");
// Act & Assert
mockMvc.perform(post("/api/v1/users")
…(truncated)