spring-framework-patterns

Spring Framework Patterns

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "spring-framework-patterns" with this command: npx skills add clostaunau/holiday-card/clostaunau-holiday-card-spring-framework-patterns

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

// 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&#x3C;String, Object> cache = new ConcurrentHashMap&#x3C;>();
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&#x3C;Page&#x3C;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&#x3C;UserDTO> users = userService.findAll(pageable);
    return ResponseEntity.ok(users);
}

@GetMapping("/{id}")
public ResponseEntity&#x3C;UserDTO> getUserById(@PathVariable Long id) {
    return userService.findById(id)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
}

@PostMapping
public ResponseEntity&#x3C;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&#x3C;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&#x3C;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&#x3C;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&#x3C;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&#x3C;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&#x3C;Void> deleteOrder(@PathVariable Long id) {
    orderService.deleteOrder(id);
    return ResponseEntity.noContent().build();  // 204 No Content
}

// 202 Accepted - Async processing
@PostMapping("/{id}/process")
public ResponseEntity&#x3C;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&#x3C;User> findByEmail(String email);

boolean existsByEmail(String email);

List&#x3C;User> findByActiveTrue();

// Custom query
@Query("SELECT u FROM User u WHERE u.createdAt > :date")
List&#x3C;User> findRecentUsers(@Param("date") LocalDateTime date);

// Native query
@Query(value = "SELECT * FROM users WHERE email LIKE %:domain", nativeQuery = true)
List&#x3C;User> findByEmailDomain(@Param("domain") String domain);

// Modifying query
@Modifying
@Query("UPDATE User u SET u.active = false WHERE u.lastLoginAt &#x3C; :date")
int deactivateInactiveUsers(@Param("date") LocalDateTime date);

}

Repository Hierarchy:

  • Repository<T, ID>

  • Marker interface, no methods

  • CrudRepository<T, ID>

  • Basic CRUD operations

  • PagingAndSortingRepository<T, ID>

  • Adds pagination and sorting

  • JpaRepository<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&#x3C;User> hasEmail(String email) {
    return (root, query, cb) ->
        email == null ? null : cb.equal(root.get("email"), email);
}

public static Specification&#x3C;User> isActive() {
    return (root, query, cb) -> cb.isTrue(root.get("active"));
}

public static Specification&#x3C;User> createdAfter(LocalDateTime date) {
    return (root, query, cb) ->
        date == null ? null : cb.greaterThan(root.get("createdAt"), date);
}

public static Specification&#x3C;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&#x3C;User> searchUsers(UserSearchCriteria criteria, Pageable pageable) {
    Specification&#x3C;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&#x3C;Page&#x3C;UserDTO>> getUsers(
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "20") int size,
        @RequestParam(defaultValue = "id,desc") String[] sort) {

    // Parse sort parameters
    List&#x3C;Sort.Order> orders = Arrays.stream(sort)
            .map(s -> {
                String[] parts = s.split(",");
                String property = parts[0];
                Sort.Direction direction = parts.length > 1 &#x26;&#x26; 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&#x3C;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&#x3C;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&#x3C;UserDTO> findById(Long id) {
    return userRepository.findById(id).map(this::mapToDTO);
}

@Transactional(readOnly = true)
public Page&#x3C;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&#x3C;UserDTO> toDTOs(List&#x3C;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&#x3C;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&#x3C;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&#x3C;ValidationErrorResponse> handleValidationErrors(MethodArgumentNotValidException ex) {
    log.warn("Validation failed: {}", ex.getMessage());

    Map&#x3C;String, String> errors = new HashMap&#x3C;>();
    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&#x3C;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&#x3C;String, Object> claims = new HashMap&#x3C;>();
    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 &#x26;&#x26; 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 &#x26;&#x26; 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&#x3C;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 &#x26; 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 &#x26; 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 &#x26; 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:

@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&#x3C;User> found = userRepository.findByEmail("test@example.com");

    // Assert
    assertTrue(found.isPresent());
    assertEquals("test@example.com", found.get().getEmail());
}

@Test
void findByEmail_WithNonExistingEmail_ShouldReturnEmpty() {
    // Act
    Optional&#x3C;User> 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:

@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 &#x26; 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 &#x26; 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:

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:

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:

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:

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

@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&#x3C;UserDTO> 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:

@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&#x3C;Product> findByCategoryAndPrice(String category, String priceRange) {
    // ...
}

// Custom KeyGenerator
@Cacheable(value = "products", keyGenerator = "customKeyGenerator")
public List&#x3C;Product> 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:

@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;
}

}

@Service @RequiredArgsConstructor public class EmailService {

@Async("taskExecutor")
public void sendWelcomeEmail(User user) {
    // Runs asynchronously
    System.out.println("Sending email to: " + user.getEmail());
    // Email sending logic
}

@Async("taskExecutor")
public CompletableFuture&#x3C;EmailStatus> sendEmailWithResult(String to, String subject, String body) {
    // Async with result
    try {
        // Send email
        return CompletableFuture.completedFuture(EmailStatus.SENT);
    } catch (Exception e) {
        return CompletableFuture.completedFuture(EmailStatus.FAILED);
    }
}

}

11.2 Event-Driven Architecture

Rule: Use Spring Events for decoupling components.

✅ Good:

// Event public class UserRegisteredEvent { private final User user;

public UserRegisteredEvent(User user) {
    this.user = user;
}

public User getUser() {
    return user;
}

}

// Publisher @Service @RequiredArgsConstructor public class UserService {

private final UserRepository userRepository;
private final ApplicationEventPublisher eventPublisher;

@Transactional
public UserDTO register(RegistrationRequest request) {
    User user = new User();
    user.setEmail(request.getEmail());
    User saved = userRepository.save(user);

    // Publish event
    eventPublisher.publishEvent(new UserRegisteredEvent(saved));

    return mapToDTO(saved);
}

}

// Listeners @Component @Slf4j public class UserRegistrationEventListener {

@Async
@EventListener
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleUserRegistered(UserRegisteredEvent event) {
    log.info("User registered: {}", event.getUser().getEmail());
    // Send welcome email
}

}

@Component public class AuditEventListener {

@EventListener
public void handleUserRegistered(UserRegisteredEvent event) {
    // Log to audit trail
}

}

Task 12: Implement AOP

12.1 Logging Aspect

Rule: Use AOP for cross-cutting concerns like logging, auditing, and performance monitoring.

✅ Good:

@Aspect @Component @Slf4j public class LoggingAspect {

@Around("execution(* com.example.app.service.*.*(..))")
public Object logServiceMethods(ProceedingJoinPoint joinPoint) throws Throwable {
    String methodName = joinPoint.getSignature().getName();
    String className = joinPoint.getTarget().getClass().getSimpleName();

    log.debug("Entering {}.{}", className, methodName);

    long startTime = System.currentTimeMillis();

    try {
        Object result = joinPoint.proceed();
        long duration = System.currentTimeMillis() - startTime;

        log.debug("Exiting {}.{} - Duration: {}ms", className, methodName, duration);

        return result;
    } catch (Exception e) {
        log.error("Exception in {}.{}: {}", className, methodName, e.getMessage());
        throw e;
    }
}

@Before("@annotation(com.example.app.annotation.Audit)")
public void auditMethod(JoinPoint joinPoint) {
    String methodName = joinPoint.getSignature().getName();
    Object[] args = joinPoint.getArgs();

    log.info("Audit: Method {} called with args: {}", methodName, Arrays.toString(args));
}

}

// Custom annotation @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Audit { }

// Usage @Service public class UserService {

@Audit
public UserDTO createUser(CreateUserRequest request) {
    // Method will be audited
}

}

Common Anti-Patterns

Anti-Pattern 1: Field Injection

❌ Bad:

@Service public class UserService { @Autowired private UserRepository userRepository; // Field injection }

✅ Good:

@Service @RequiredArgsConstructor public class UserService { private final UserRepository userRepository; // Constructor injection }

Anti-Pattern 2: Service Layer Bypassing

❌ Bad:

@RestController public class UserController { @Autowired private UserRepository userRepository; // Controller accessing repository directly

@GetMapping("/users")
public List&#x3C;User> getUsers() {
    return userRepository.findAll();  // Business logic in controller
}

}

✅ Good:

@RestController @RequiredArgsConstructor public class UserController { private final UserService userService; // Access through service layer

@GetMapping("/users")
public ResponseEntity&#x3C;List&#x3C;UserDTO>> getUsers() {
    return ResponseEntity.ok(userService.findAll());
}

}

Anti-Pattern 3: Exposing Entities Directly

❌ Bad:

@RestController public class UserController {

@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
    return userService.findById(id);  // Exposing entity
}

}

✅ Good:

@RestController public class UserController {

@GetMapping("/users/{id}")
public ResponseEntity&#x3C;UserDTO> getUser(@PathVariable Long id) {
    return ResponseEntity.ok(userService.findById(id));  // Return DTO
}

}

Anti-Pattern 4: @Transactional on Repository Methods

❌ Bad:

@Repository public interface UserRepository extends JpaRepository<User, Long> {

@Transactional  // Don't put @Transactional on repository
@Query("UPDATE User u SET u.active = false WHERE u.id = :id")
void deactivate(@Param("id") Long id);

}

✅ Good:

@Service @RequiredArgsConstructor public class UserService {

private final UserRepository userRepository;

@Transactional  // Put @Transactional on service methods
public void deactivateUser(Long id) {
    User user = userRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("User", id));
    user.setActive(false);
    userRepository.save(user);
}

}

Anti-Pattern 5: Not Using Connection Pooling

❌ Bad:

No connection pooling configuration

spring: datasource: url: jdbc:postgresql://localhost:5432/myapp

✅ Good:

spring: datasource: url: jdbc:postgresql://localhost:5432/myapp hikari: maximum-pool-size: 10 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000

Anti-Pattern 6: Too Many Responsibilities in Controllers

❌ Bad:

@RestController public class UserController {

@PostMapping("/users")
public User createUser(@RequestBody User user) {
    // Validation in controller
    if (user.getEmail() == null) {
        throw new RuntimeException("Email required");
    }

    // Business logic in controller
    if (userRepository.existsByEmail(user.getEmail())) {
        throw new RuntimeException("Email exists");
    }

    // Data access in controller
    User saved = userRepository.save(user);

    // Email sending in controller
    emailService.send(user.getEmail(), "Welcome!");

    return saved;
}

}

✅ Good:

@RestController @RequiredArgsConstructor public class UserController {

private final UserService userService;

@PostMapping("/users")
public ResponseEntity&#x3C;UserDTO> createUser(@Valid @RequestBody CreateUserRequest request) {
    UserDTO created = userService.createUser(request);
    return ResponseEntity.created(location).body(created);
}

}

@Service @RequiredArgsConstructor public class UserService {

private final UserRepository userRepository;
private final EmailService emailService;

@Transactional
public UserDTO createUser(CreateUserRequest request) {
    // All business logic in service
    validateEmail(request.getEmail());
    User user = createUserEntity(request);
    User saved = userRepository.save(user);
    emailService.sendWelcomeEmail(saved);
    return mapToDTO(saved);
}

}

Anti-Pattern 7: Storing Business Logic in Entities

❌ Bad:

@Entity public class Order {

@Autowired  // Don't inject dependencies in entities!
private PaymentService paymentService;

public void process() {
    // Business logic in entity - BAD!
    if (this.total > 1000) {
        paymentService.processLargeOrder(this);
    } else {
        paymentService.processSmallOrder(this);
    }
}

}

✅ Good:

@Entity public class Order { // Pure data model - no business logic private Long id; private BigDecimal total; private OrderStatus status; // Getters and setters }

@Service @RequiredArgsConstructor public class OrderService {

private final PaymentService paymentService;
private final OrderRepository orderRepository;

@Transactional
public void processOrder(Long orderId) {
    Order order = orderRepository.findById(orderId)
            .orElseThrow(() -> new ResourceNotFoundException("Order", orderId));

    if (order.getTotal().compareTo(BigDecimal.valueOf(1000)) > 0) {
        paymentService.processLargeOrder(order);
    } else {
        paymentService.processSmallOrder(order);
    }
}

}

Checklist

Use this checklist during code reviews:

Dependency Injection

  • Constructor injection used for required dependencies

  • Fields are final where possible

  • No field injection (@Autowired on fields)

  • No circular dependencies

  • Setter injection only for optional dependencies

Component Structure

  • Correct stereotype annotations (@Service, @Repository, @Controller, @RestController)

  • Controllers are thin (delegate to services)

  • Business logic in service layer

  • Data access through repositories only

  • Proper layering (Controller → Service → Repository)

REST API Design

  • RESTful URL structure

  • Correct HTTP methods (GET, POST, PUT, DELETE)

  • Correct HTTP status codes

  • DTOs used for requests/responses (not entities)

  • @Valid used for request validation

  • Pagination supported for list endpoints

  • Location header for created resources

Data Access

  • Appropriate repository interface (JpaRepository, etc.)

  • Query methods follow Spring Data naming conventions

  • @Transactional on service methods (not repositories)

  • readOnly=true for read operations

  • Connection pooling configured (HikariCP)

  • No N+1 query problems

Configuration

  • External configuration in application.yml

  • Profile-specific configuration files

  • @ConfigurationProperties for type-safe config

  • Sensitive data in environment variables

  • Proper defaults for all properties

Exception Handling

  • Custom exceptions for business errors

  • @RestControllerAdvice for global exception handling

  • Proper HTTP status codes for different exceptions

  • Validation errors handled properly

  • No generic Exception catching without re-throwing

Security

  • Security configuration in @Configuration class

  • Password encoder configured (BCrypt)

  • Method security enabled where needed

  • CSRF protection enabled (or disabled with justification)

  • JWT properly implemented for stateless auth

Testing

  • Unit tests for services with mocked dependencies

  • Integration tests with @SpringBootTest

  • Repository tests with @DataJpaTest

  • Controller tests with @WebMvcTest

  • Test coverage > 80%

Performance

  • Caching enabled where appropriate

  • Async processing for long-running operations

  • Lazy loading configured properly

  • Database indexes on frequently queried fields

Code Quality

  • No code duplication

  • Meaningful names for classes, methods, variables

  • Methods are focused and not too long

  • Proper logging (not System.out.println)

  • No commented-out code

Related Skills

  • uncle-duke-java: Java code review agent that uses this skill as reference

References

Official Documentation

  • Spring Framework Documentation

  • Spring Boot Documentation

  • Spring Data JPA Documentation

  • Spring Security Documentation

Best Practices Guides

  • Baeldung Spring Tutorials

  • Spring Boot Best Practices

  • RESTful API Design Best Practices

Version: 1.0 Last Updated: 2025-12-24 Maintainer: Development Team

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

java-best-practices

No summary provided by upstream source.

Repository SourceNeeds Review
General

django-conventions

No summary provided by upstream source.

Repository SourceNeeds Review
General

fastapi-patterns

No summary provided by upstream source.

Repository SourceNeeds Review