🎯

spring-framework-patterns

🎯Skill

from clostaunau/holiday-card

VibeIndex|
What it does

Provides comprehensive Spring Framework and Spring Boot best practices, patterns, and guidelines for enterprise Java application development and code reviews.

spring-framework-patterns

Installation

Install skill:
npx skills add https://github.com/clostaunau/holiday-card --skill spring-framework-patterns
7
Last UpdatedDec 26, 2025

Skill Details

SKILL.md

Comprehensive Spring Framework and Spring Boot best practices including dependency injection patterns, bean lifecycle and scopes, REST API development, Spring Data JPA, service layer design, Spring Security, testing strategies, caching, AOP, async processing, error handling, and common anti-patterns. Essential reference for code reviews and Spring Boot application development.

Overview

# 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:

```java

@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:

```java

@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):

```java

@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:

```java

@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:

```java

@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:

```java

// 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 container
  • prototype: New instance each time bean is requested
  • request: One instance per HTTP request (web applications)
  • session: One instance per HTTP session (web applications)
  • application: One instance per ServletContext (web applications)

βœ… Good:

```java

// 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 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:

```java

// 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:

```java

@Service

public class CacheService {

private final Map 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.

```java

@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:

```java

@Repository // Data access - enables exception translation

public interface UserRepository extends JpaRepository {

}

@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:

```java

@RestController

@RequestMapping("/api/v1/users")

@RequiredArgsConstructor

public class UserController {

private final UserService userService;

@GetMapping

public ResponseEntity> 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 users = userService.findAll(pageable);

return ResponseEntity.ok(users);

}

@GetMapping("/{id}")

public ResponseEntity getUserById(@PathVariable Long id) {

return userService.findById(id)

.map(ResponseEntity::ok)

.orElse(ResponseEntity.notFound().build());

}

@PostMapping

public ResponseEntity 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 updateUser(

@PathVariable Long id,

@Valid @RequestBody UpdateUserRequest request) {

return userService.updateUser(id, request)

.map(ResponseEntity::ok)

.orElse(ResponseEntity.notFound().build());

}

@DeleteMapping("/{id}")

public ResponseEntity 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:

```java

@RestController

public class UserController {

@Autowired

private UserRepository userRepository; // Controller accessing repository directly!

@GetMapping("/getUsers") // Non-RESTful URL

public List 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:

```java

// 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.

```java

@RestController

@RequestMapping("/api/v1/orders")

@RequiredArgsConstructor

public class OrderController {

private final OrderService orderService;

// 200 OK - Successful GET/PUT

@GetMapping("/{id}")

public ResponseEntity 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 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 deleteOrder(@PathVariable Long id) {

orderService.deleteOrder(id);

return ResponseEntity.noContent().build(); // 204 No Content

}

// 202 Accepted - Async processing

@PostMapping("/{id}/process")

public ResponseEntity 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:

```java

// Simple CRUD - JpaRepository

@Repository

public interface UserRepository extends JpaRepository {

// Query methods - Spring Data generates implementation

Optional findByEmail(String email);

boolean existsByEmail(String email);

List findByActiveTrue();

// Custom query

@Query("SELECT u FROM User u WHERE u.createdAt > :date")

List findRecentUsers(@Param("date") LocalDateTime date);

// Native query

@Query(value = "SELECT * FROM users WHERE email LIKE %:domain", nativeQuery = true)

List 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 - Marker interface, no methods
  • CrudRepository - Basic CRUD operations
  • PagingAndSortingRepository - Adds pagination and sorting
  • JpaRepository - JPA-specific features (flush, batch operations)

#### 4.2 Custom Queries with Specifications

Rule: Use Specifications for dynamic queries instead of building query strings.

βœ… Good:

```java

// Specification

public class UserSpecifications {

public static Specification hasEmail(String email) {

return (root, query, cb) ->

email == null ? null : cb.equal(root.get("email"), email);

}

public static Specification isActive() {

return (root, query, cb) -> cb.isTrue(root.get("active"));

}

public static Specification createdAfter(LocalDateTime date) {

return (root, query, cb) ->

date == null ? null : cb.greaterThan(root.get("createdAt"), date);

}

public static Specification 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, JpaSpecificationExecutor {

}

// Service

@Service

@RequiredArgsConstructor

public class UserService {

private final UserRepository userRepository;

public Page searchUsers(UserSearchCriteria criteria, Pageable pageable) {

Specification 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:

```java

@RestController

@RequestMapping("/api/v1/users")

@RequiredArgsConstructor

public class UserController {

private final UserService userService;

@GetMapping

public ResponseEntity> getUsers(

@RequestParam(defaultValue = "0") int page,

@RequestParam(defaultValue = "20") int size,

@RequestParam(defaultValue = "id,desc") String[] sort) {

// Parse sort parameters

List 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 users = userService.findAll(pageable);

return ResponseEntity.ok(users);

}

}

```

#### 4.4 Transaction Management

Rule: Use @Transactional on service methods, not repository methods.

βœ… Good:

```java

@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 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:

```java

@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:

```java

// 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 findById(Long id) {

return userRepository.findById(id).map(this::mapToDTO);

}

@Transactional(readOnly = true)

public Page 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:

```java

@Mapper(componentModel = "spring")

public interface UserMapper {

UserDTO toDTO(User user);

List toDTOs(List 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:

```java

@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:

```java

// 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:

```java

@RestControllerAdvice

@Slf4j

public class GlobalExceptionHandler {

@ExceptionHandler(ResourceNotFoundException.class)

public ResponseEntity 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 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 handleValidationErrors(MethodArgumentNotValidException ex) {

log.warn("Validation failed: {}", ex.getMessage());

Map 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 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 fieldErrors;

}

```

Task 7: Implement Spring Security

#### 7.1 Security Configuration

Rule: Use security configuration classes for centralized security setup.

βœ… Good (Spring Security 6+):

```java

@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:

```java

@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 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:

```java

@Service

@RequiredArgsConstructor

public class UserService {

private final UserRepository userRepository;

@PreAuthorize("hasRole('ADMIN')")

public List 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:

```java

@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:

```java

@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")

.contentType(MediaType.APPLICATION_JSON)

.content(objectMapper.writeValueAsString(request)))

.andExpect(status().isCreated())

.andExpect(header().exists("Location"))

.andExpect(jsonPath("$.email").value("test@example.com"))

.andExpect(jsonPath("$.name").value("Test User"))

.andExpect(jsonPath("$.id").exists());

// Verify database

assertEquals(1, userRepository.count());

}

@Test

void createUser_WithInvalidEmail_ShouldReturn400() throws Exception {

// Arrange

CreateUserRequest request = new CreateUserRequest();

request.setEmail("invalid-email");

request.setName("Test User");

request.setPassword("password123");

// Act & Assert

mockMvc.perform(post("/api/v1/users")

.contentType(MediaType.APPLICATION_JSON)

.content(objectMapper.writeValueAsString(request)))

.andExpect(status().isBadRequest())

.andExpect(jsonPath("$.fieldErrors.email").exists());

}

}

```

#### 8.3 Testing Repositories with @DataJpaTest

Rule: Use @DataJpaTest for repository tests with in-memory database.

βœ… Good:

```java

@DataJpaTest

class UserRepositoryTest {

@Autowired

private UserRepository userRepository;

@Autowired

private TestEntityManager entityManager;

@Test

void findByEmail_WithExistingEmail_ShouldReturnUser() {

// Arrange

User user = new User();

user.setEmail("test@example.com");

user.setName("Test User");

entityManager.persist(user);

entityManager.flush();

// Act

Optional found = userRepository.findByEmail("test@example.com");

// Assert

assertTrue(found.isPresent());

assertEquals("test@example.com", found.get().getEmail());

}

@Test

void findByEmail_WithNonExistingEmail_ShouldReturnEmpty() {

// Act

Optional found = userRepository.findByEmail("nonexistent@example.com");

// Assert

assertFalse(found.isPresent());

}

}

```

#### 8.4 Testing Controllers with @WebMvcTest

Rule: Use @WebMvcTest for controller tests without full context.

βœ… Good:

```java

@WebMvcTest(UserController.class)

class UserControllerTest {

@Autowired

private MockMvc mockMvc;

@MockBean

private UserService userService;

@Autowired

private ObjectMapper objectMapper;

@Test

void getUserById_WithExistingId_ShouldReturnUser() throws Exception {

// Arrange

UserDTO user = new UserDTO();

user.setId(1L);

user.setEmail("test@example.com");

when(userService.findById(1L)).thenReturn(Optional.of(user));

// Act & Assert

mockMvc.perform(get("/api/v1/users/1"))

.andExpect(status().isOk())

.andExpect(jsonPath("$.id").value(1))

.andExpect(jsonPath("$.email").value("test@example.com"));

verify(userService).findById(1L);

}

@Test

void getUserById_WithNonExistingId_ShouldReturn404() throws Exception {

// Arrange

when(userService.findById(999L)).thenReturn(Optional.empty());

// Act & Assert

mockMvc.perform(get("/api/v1/users/999"))

.andExpect(status().isNotFound());

}

}

```

Task 9: Implement Configuration Management

#### 9.1 External Configuration

Rule: Use application.yml for configuration, support multiple profiles.

βœ… Good application.yml:

```yaml

spring:

application:

name: my-spring-app

profiles:

active: dev

datasource:

url: ${DB_URL:jdbc:postgresql://localhost:5432/myapp}

username: ${DB_USERNAME:postgres}

password: ${DB_PASSWORD:password}

hikari:

maximum-pool-size: 10

minimum-idle: 5

connection-timeout: 30000

idle-timeout: 600000

max-lifetime: 1800000

jpa:

hibernate:

ddl-auto: validate

show-sql: false

properties:

hibernate:

format_sql: true

dialect: org.hibernate.dialect.PostgreSQLDialect

cache:

type: caffeine

caffeine:

spec: maximumSize=500,expireAfterAccess=600s

server:

port: 8080

error:

include-message: always

include-binding-errors: always

include-stacktrace: on_param

include-exception: false

logging:

level:

root: INFO

com.example.app: DEBUG

pattern:

console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"

# Custom application properties

app:

jwt:

secret: ${JWT_SECRET:default-secret-change-in-production}

expiration: 3600000

email:

from: noreply@example.com

features:

new-ui: false

```

application-dev.yml:

```yaml

spring:

jpa:

show-sql: true

hibernate:

ddl-auto: update

logging:

level:

com.example.app: DEBUG

org.hibernate.SQL: DEBUG

org.hibernate.type.descriptor.sql.BasicBinder: TRACE

app:

features:

new-ui: true

```

application-prod.yml:

```yaml

spring:

jpa:

show-sql: false

hibernate:

ddl-auto: validate

logging:

level:

root: WARN

com.example.app: INFO

app:

jwt:

expiration: 7200000 # 2 hours in production

```

#### 9.2 @ConfigurationProperties

Rule: Use @ConfigurationProperties for type-safe configuration.

βœ… Good:

```java

@ConfigurationProperties(prefix = "app")

@Validated

public class AppProperties {

@NotNull

private Jwt jwt;

@NotNull

private Email email;

@NotNull

private Features features;

@Data

public static class Jwt {

@NotBlank

private String secret;

@Min(60000) // At least 1 minute

private long expiration;

}

@Data

public static class Email {

@Email

private String from;

}

@Data

public static class Features {

private boolean newUi;

}

// Getters and setters

}

// Enable configuration properties

@Configuration

@EnableConfigurationProperties(AppProperties.class)

public class AppConfig {

}

// Usage

@Service

@RequiredArgsConstructor

public class EmailService {

private final AppProperties appProperties;

public void sendEmail(String to, String subject, String body) {

String from = appProperties.getEmail().getFrom();

// Send email logic

}

}

```

Task 10: Implement Caching

#### 10.1 Enable Caching

Rule: Use Spring Cache abstraction for declarative caching.

βœ… Good:

```java

@Configuration

@EnableCaching

public class CacheConfig {

@Bean

public CacheManager cacheManager() {

CaffeineCacheManager cacheManager = new CaffeineCacheManager(

"users", "products", "orders"

);

cacheManager.setCaffeine(Caffeine.newBuilder()

.maximumSize(1000)

.expireAfterWrite(10, TimeUnit.MINUTES)

.recordStats());

return cacheManager;

}

}

@Service

@RequiredArgsConstructor

public class UserService {

private final UserRepository userRepository;

@Cacheable(value = "users", key = "#id")

public Optional findById(Long id) {

return userRepository.findById(id)

.map(this::mapToDTO);

}

@CachePut(value = "users", key = "#result.id")

public UserDTO updateUser(Long id, UpdateUserRequest request) {

User user = userRepository.findById(id)

.orElseThrow(() -> new ResourceNotFoundException("User", id));

user.setName(request.getName());

User updated = userRepository.save(user);

return mapToDTO(updated);

}

@CacheEvict(value = "users", key = "#id")

public void deleteUser(Long id) {

userRepository.deleteById(id);

}

@CacheEvict(value = "users", allEntries = true)

public void clearUserCache() {

// Cache cleared

}

}

```

#### 10.2 Cache Key Strategies

Rule: Use explicit cache keys for complex scenarios.

βœ… Good:

```java

@Service

public class ProductService {

// Simple key

@Cacheable(value = "products", key = "#id")

public Product findById(Long id) {

// ...

}

// Composite key

@Cacheable(value = "products", key = "#category + '-' + #priceRange")

public List findByCategoryAndPrice(String category, String priceRange) {

// ...

}

// Custom KeyGenerator

@Cacheable(value = "products", keyGenerator = "customKeyGenerator")

public List search(ProductSearchCriteria criteria) {

// ...

}

}

@Component("customKeyGenerator")

public class CustomKeyGenerator implements org.springframework.cache.interceptor.KeyGenerator {

@Override

public Object generate(Object target, Method method, Object... params) {

return target.getClass().getSimpleName() + "_"

+ method.getName() + "_"

+ Arrays.stream(params).map(String::valueOf).collect(Collectors.joining("_"));

}

}

```

Task 11: Implement Async Processing

#### 11.1 Enable Async

Rule: Use @Async for non-blocking operations.

βœ… Good:

```java

@Configuration

@EnableAsync

public class AsyncConfig {

@Bean(name = "taskExecutor")

public Executor taskExecutor() {

ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

executor.setCorePoolSize(2);

executor.setMaxPoolSize(10);

executor.setQueueCapacity(100);

executor.setThreadNamePrefix("async-");

executor.initialize();

return executor;

}