| name | spring-framework-patterns |
| description | 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. |
| allowed-tools | Read, Write, Grep, Glob |
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;
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 {
@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
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;
private SmsService smsService;
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
private PaymentService paymentService;
}
@Service
public class PaymentService {
@Autowired
@Lazy
private OrderService orderService;
}
✅ Good:
@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:
@Service
@Scope("singleton")
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
@Component
@Scope("prototype")
public class ReportGenerator {
private final List<String> reportLines = new ArrayList<>();
public void addLine(String line) {
reportLines.add(line);
}
public String generate() {
return String.join("\n", reportLines);
}
}
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext {
private String requestId;
private String userId;
}
❌ Bad:
@Service
public class UserService {
private User currentUser;
public void processUser(User user) {
this.currentUser = user;
}
}
2.2 Bean Lifecycle Callbacks
Rule: Use @PostConstruct for initialization, @PreDestroy for cleanup.
✅ Good:
@Service
public class CacheService {
private final Map<String, Object> cache = new ConcurrentHashMap<>();
private ScheduledExecutorService scheduler;
@PostConstruct
public void initialize() {
System.out.println("Initializing cache service...");
scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(this::cleanupExpiredEntries, 1, 1, TimeUnit.HOURS);
}
@PreDestroy
public void cleanup() {
System.out.println("Cleaning up cache service...");
cache.clear();
if (scheduler != null) {
scheduler.shutdown();
}
}
private void cleanupExpiredEntries() {
}
}
2.3 Component Stereotypes
Rule: Use the most specific stereotype annotation.
@Component
@Service
@Repository
@Controller
@RestController
@Configuration
✅ Good:
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
}
@Configuration
public class SecurityConfig {
}
Task 3: Design REST API Controllers
3.1 Controller Structure
Rule: Keep controllers thin - delegate business logic to services.
✅ Good:
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping
public ResponseEntity<Page<UserDTO>> getAllUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "id") String sortBy) {
Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy));
Page<UserDTO> users = userService.findAll(pageable);
return ResponseEntity.ok(users);
}
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getUserById(@PathVariable Long id) {
return userService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<UserDTO> createUser(@Valid @RequestBody CreateUserRequest request) {
UserDTO created = userService.createUser(request);
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(created.getId())
.toUri();
return ResponseEntity.created(location).body(created);
}
@PutMapping("/{id}")
public ResponseEntity<UserDTO> updateUser(
@PathVariable Long id,
@Valid @RequestBody UpdateUserRequest request) {
return userService.updateUser(id, request)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
}
Why good:
- RESTful URL structure
- Proper HTTP methods and status codes
- Pagination and sorting support
- Validation with @Valid
- Location header for created resources
- Delegates logic to service layer
❌ Bad:
@RestController
public class UserController {
@Autowired
private UserRepository userRepository;
@GetMapping("/getUsers")
public List<User> getUsers() {
return userRepository.findAll();
}
@PostMapping("/createUser")
public User createUser(@RequestBody User user) {
if (userRepository.findByEmail(user.getEmail()).isPresent()) {
throw new RuntimeException("Email exists");
}
return userRepository.save(user);
}
}
3.2 Request/Response Patterns
Rule: Use DTOs for API contracts. Never expose entities directly.
✅ Good:
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;
}
public class UserDTO {
private Long id;
private String email;
private String name;
private LocalDateTime createdAt;
private boolean active;
}
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final UserMapper userMapper;
public UserDTO createUser(CreateUserRequest request) {
if (userRepository.existsByEmail(request.getEmail())) {
throw new EmailAlreadyExistsException(request.getEmail());
}
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());
User saved = userRepository.save(user);
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;
@GetMapping("/{id}")
public ResponseEntity<OrderDTO> getOrder(@PathVariable Long id) {
return orderService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<OrderDTO> createOrder(@Valid @RequestBody CreateOrderRequest request) {
OrderDTO created = orderService.createOrder(request);
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(created.getId())
.toUri();
return ResponseEntity.created(location).body(created);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteOrder(@PathVariable Long id) {
orderService.deleteOrder(id);
return ResponseEntity.noContent().build();
}
@PostMapping("/{id}/process")
public ResponseEntity<Void> processOrder(@PathVariable Long id) {
orderService.processOrderAsync(id);
return ResponseEntity.accepted().build();
}
}
Task 4: Implement Data Access Layer with Spring Data JPA
4.1 Repository Interfaces
Rule: Extend appropriate Spring Data interface based on needs.
✅ Good:
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
boolean existsByEmail(String email);
List<User> findByActiveTrue();
@Query("SELECT u FROM User u WHERE u.createdAt > :date")
List<User> findRecentUsers(@Param("date") LocalDateTime date);
@Query(value = "SELECT * FROM users WHERE email LIKE %:domain", nativeQuery = true)
List<User> findByEmailDomain(@Param("domain") String domain);
@Modifying
@Query("UPDATE User u SET u.active = false WHERE u.lastLoginAt < :date")
int deactivateInactiveUsers(@Param("date") LocalDateTime date);
}
Repository Hierarchy:
Repository<T, ID> - Marker interface, no 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:
public class UserSpecifications {
public static Specification<User> hasEmail(String email) {
return (root, query, cb) ->
email == null ? null : cb.equal(root.get("email"), email);
}
public static Specification<User> isActive() {
return (root, query, cb) -> cb.isTrue(root.get("active"));
}
public static Specification<User> createdAfter(LocalDateTime date) {
return (root, query, cb) ->
date == null ? null : cb.greaterThan(root.get("createdAt"), date);
}
public static Specification<User> nameLike(String name) {
return (root, query, cb) ->
name == null ? null : cb.like(cb.lower(root.get("name")), "%" + name.toLowerCase() + "%");
}
}
@Repository
public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {
}
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
public Page<User> searchUsers(UserSearchCriteria criteria, Pageable pageable) {
Specification<User> spec = Specification.where(null);
if (criteria.getEmail() != null) {
spec = spec.and(UserSpecifications.hasEmail(criteria.getEmail()));
}
if (criteria.isActiveOnly()) {
spec = spec.and(UserSpecifications.isActive());
}
if (criteria.getCreatedAfter() != null) {
spec = spec.and(UserSpecifications.createdAfter(criteria.getCreatedAfter()));
}
if (criteria.getName() != null) {
spec = spec.and(UserSpecifications.nameLike(criteria.getName()));
}
return userRepository.findAll(spec, pageable);
}
}
4.3 Pagination and Sorting
Rule: Always support pagination for list endpoints.
✅ Good:
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping
public ResponseEntity<Page<UserDTO>> getUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "id,desc") String[] sort) {
List<Sort.Order> orders = Arrays.stream(sort)
.map(s -> {
String[] parts = s.split(",");
String property = parts[0];
Sort.Direction direction = parts.length > 1 && parts[1].equalsIgnoreCase("desc")
? Sort.Direction.DESC
: Sort.Direction.ASC;
return new Sort.Order(direction, property);
})
.toList();
Pageable pageable = PageRequest.of(page, size, Sort.by(orders));
Page<UserDTO> users = userService.findAll(pageable);
return ResponseEntity.ok(users);
}
}
4.4 Transaction Management
Rule: Use @Transactional on service methods, not repository methods.
✅ Good:
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentRepository paymentRepository;
private final EmailService emailService;
@Transactional
public OrderDTO createOrder(CreateOrderRequest request) {
Order order = new Order();
order.setUserId(request.getUserId());
order.setStatus(OrderStatus.PENDING);
Order savedOrder = orderRepository.save(order);
Payment payment = new Payment();
payment.setOrderId(savedOrder.getId());
payment.setAmount(request.getAmount());
paymentRepository.save(payment);
emailService.sendOrderConfirmation(savedOrder);
return mapToDTO(savedOrder);
}
@Transactional(readOnly = true)
public Optional<OrderDTO> findById(Long id) {
return orderRepository.findById(id)
.map(this::mapToDTO);
}
@Transactional(isolation = Isolation.SERIALIZABLE)
public void processPayment(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
order.setStatus(OrderStatus.PAID);
orderRepository.save(order);
}
}
Transaction Propagation:
@Service
public class UserService {
@Transactional(propagation = Propagation.REQUIRED)
public void updateUser(User user) {
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void auditLog(String action) {
}
@Transactional(propagation = Propagation.MANDATORY)
public void criticalOperation() {
}
}
Task 5: Design Service Layer
5.1 Service Boundaries
Rule: Services should represent business capabilities, not data access.
✅ Good:
@Service
@RequiredArgsConstructor
public class UserRegistrationService {
private final UserRepository userRepository;
private final EmailService emailService;
private final PasswordEncoder passwordEncoder;
@Transactional
public UserDTO register(RegistrationRequest request) {
validateEmailNotExists(request.getEmail());
User user = createUser(request);
User saved = userRepository.save(user);
emailService.sendWelcomeEmail(saved.getEmail());
return mapToDTO(saved);
}
private void validateEmailNotExists(String email) {
if (userRepository.existsByEmail(email)) {
throw new EmailAlreadyExistsException(email);
}
}
private User createUser(RegistrationRequest request) {
User user = new User();
user.setEmail(request.getEmail());
user.setName(request.getName());
user.setPassword(passwordEncoder.encode(request.getPassword()));
return user;
}
}
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
@Transactional(readOnly = true)
public Optional<UserDTO> findById(Long id) {
return userRepository.findById(id).map(this::mapToDTO);
}
@Transactional(readOnly = true)
public Page<UserDTO> findAll(Pageable pageable) {
return userRepository.findAll(pageable).map(this::mapToDTO);
}
}
5.2 DTO Mapping
Rule: Keep entity-to-DTO mapping logic in one place.
✅ Good with MapStruct:
@Mapper(componentModel = "spring")
public interface UserMapper {
UserDTO toDTO(User user);
List<UserDTO> toDTOs(List<User> users);
@Mapping(target = "id", ignore = true)
@Mapping(target = "createdAt", ignore = true)
User toEntity(CreateUserRequest request);
}
@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:
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;
}
}
public class ResourceNotFoundException extends BusinessException {
public ResourceNotFoundException(String resourceName, Long id) {
super(String.format("%s with id %d not found", resourceName, id), "RESOURCE_NOT_FOUND");
}
}
public class EmailAlreadyExistsException extends BusinessException {
public EmailAlreadyExistsException(String email) {
super(String.format("Email %s already exists", email), "EMAIL_EXISTS");
}
}
public class InsufficientBalanceException extends BusinessException {
public InsufficientBalanceException(Long accountId) {
super(String.format("Insufficient balance in account %d", accountId), "INSUFFICIENT_BALANCE");
}
}
6.2 Global Exception Handler with @ControllerAdvice
Rule: Centralize exception handling in @ControllerAdvice.
✅ Good:
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleResourceNotFound(ResourceNotFoundException ex) {
log.warn("Resource not found: {}", ex.getMessage());
ErrorResponse error = ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(HttpStatus.NOT_FOUND.value())
.error("Not Found")
.message(ex.getMessage())
.errorCode(ex.getErrorCode())
.build();
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
@ExceptionHandler(EmailAlreadyExistsException.class)
public ResponseEntity<ErrorResponse> handleEmailAlreadyExists(EmailAlreadyExistsException ex) {
log.warn("Email already exists: {}", ex.getMessage());
ErrorResponse error = ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(HttpStatus.CONFLICT.value())
.error("Conflict")
.message(ex.getMessage())
.errorCode(ex.getErrorCode())
.build();
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ValidationErrorResponse> handleValidationErrors(MethodArgumentNotValidException ex) {
log.warn("Validation failed: {}", ex.getMessage());
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
ValidationErrorResponse response = ValidationErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(HttpStatus.BAD_REQUEST.value())
.error("Validation Failed")
.message("Invalid request parameters")
.fieldErrors(errors)
.build();
return ResponseEntity.badRequest().body(response);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) {
log.error("Unexpected error occurred", ex);
ErrorResponse error = ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(HttpStatus.INTERNAL_SERVER_ERROR.value())
.error("Internal Server Error")
.message("An unexpected error occurred")
.build();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}
@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}")
private long expiration;
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put("roles", userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.toList());
return Jwts.builder()
.setClaims(claims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public String getUsernameFromToken(String token) {
return getClaimsFromToken(token).getSubject();
}
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
return !isTokenExpired(token);
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
private Claims getClaimsFromToken(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
private boolean isTokenExpired(String token) {
Date expiration = getClaimsFromToken(token).getExpiration();
return expiration.before(new Date());
}
}
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider tokenProvider;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = getTokenFromRequest(request);
if (token != null && tokenProvider.validateToken(token)) {
String username = tokenProvider.getUsernameFromToken(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private String getTokenFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
7.3 Method Security
Rule: Use method security for fine-grained authorization.
✅ Good:
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
@PreAuthorize("hasRole('ADMIN')")
public List<UserDTO> findAll() {
return userRepository.findAll().stream()
.map(this::mapToDTO)
.toList();
}
@PreAuthorize("hasRole('USER') and #userId == authentication.principal.id")
public UserDTO findById(Long userId) {
return userRepository.findById(userId)
.map(this::mapToDTO)
.orElseThrow(() -> new ResourceNotFoundException("User", userId));
}
@PreAuthorize("hasAnyRole('ADMIN', 'MODERATOR')")
@PostAuthorize("returnObject.email == authentication.principal.username")
public UserDTO updateUser(Long id, UpdateUserRequest request) {
}
@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() {
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);
UserDTO result = userService.createUser(request);
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() {
CreateUserRequest request = new CreateUserRequest();
request.setEmail("existing@example.com");
when(userRepository.existsByEmail(request.getEmail())).thenReturn(true);
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
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 {
CreateUserRequest request = new CreateUserRequest();
request.setEmail("test@example.com");
request.setName("Test User");
request.setPassword("password123");
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());
assertEquals(1, userRepository.count());
}
@Test
void createUser_WithInvalidEmail_ShouldReturn400() throws Exception {
CreateUserRequest request = new CreateUserRequest();
request.setEmail("invalid-email");
request.setName("Test User");
request.setPassword("password123");
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() {
User user = new User();
user.setEmail("test@example.com");
user.setName("Test User");
entityManager.persist(user);
entityManager.flush();
Optional<User> found = userRepository.findByEmail("test@example.com");
assertTrue(found.isPresent());
assertEquals("test@example.com", found.get().getEmail());
}
@Test
void findByEmail_WithNonExistingEmail_ShouldReturnEmpty() {
Optional<User> found = userRepository.findByEmail("nonexistent@example.com");
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 {
UserDTO user = new UserDTO();
user.setId(1L);
user.setEmail("test@example.com");
when(userService.findById(1L)).thenReturn(Optional.of(user));
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 {
when(userService.findById(999L)).thenReturn(Optional.empty());
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"
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
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)
private long expiration;
}
@Data
public static class Email {
@Email
private String from;
}
@Data
public static class Features {
private boolean newUi;
}
}
@Configuration
@EnableConfigurationProperties(AppProperties.class)
public class AppConfig {
}
@Service
@RequiredArgsConstructor
public class EmailService {
private final AppProperties appProperties;
public void sendEmail(String to, String subject, String body) {
String from = appProperties.getEmail().getFrom();
}
}
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<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() {
}
}
10.2 Cache Key Strategies
Rule: Use explicit cache keys for complex scenarios.
✅ Good:
@Service
public class ProductService {
@Cacheable(value = "products", key = "#id")
public Product findById(Long id) {
}
@Cacheable(value = "products", key = "#category + '-' + #priceRange")
public List<Product> findByCategoryAndPrice(String category, String priceRange) {
}
@Cacheable(value = "products", keyGenerator = "customKeyGenerator")
public List<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) {
System.out.println("Sending email to: " + user.getEmail());
}
@Async("taskExecutor")
public CompletableFuture<EmailStatus> sendEmailWithResult(String to, String subject, String body) {
try {
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:
public class UserRegisteredEvent {
private final User user;
public UserRegisteredEvent(User user) {
this.user = user;
}
public User getUser() {
return user;
}
}
@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);
eventPublisher.publishEvent(new UserRegisteredEvent(saved));
return mapToDTO(saved);
}
}
@Component
@Slf4j
public class UserRegistrationEventListener {
@Async
@EventListener
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleUserRegistered(UserRegisteredEvent event) {
log.info("User registered: {}", event.getUser().getEmail());
}
}
@Component
public class AuditEventListener {
@EventListener
public void handleUserRegistered(UserRegisteredEvent event) {
}
}
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));
}
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Audit {
}
@Service
public class UserService {
@Audit
public UserDTO createUser(CreateUserRequest request) {
}
}
Common Anti-Patterns
Anti-Pattern 1: Field Injection
❌ Bad:
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
}
✅ Good:
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
}
Anti-Pattern 2: Service Layer Bypassing
❌ Bad:
@RestController
public class UserController {
@Autowired
private UserRepository userRepository;
@GetMapping("/users")
public List<User> getUsers() {
return userRepository.findAll();
}
}
✅ Good:
@RestController
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping("/users")
public ResponseEntity<List<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);
}
}
✅ Good:
@RestController
public class UserController {
@GetMapping("/users/{id}")
public ResponseEntity<UserDTO> getUser(@PathVariable Long id) {
return ResponseEntity.ok(userService.findById(id));
}
}
Anti-Pattern 4: @Transactional on Repository Methods
❌ Bad:
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
@Transactional
@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
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:
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) {
if (user.getEmail() == null) {
throw new RuntimeException("Email required");
}
if (userRepository.existsByEmail(user.getEmail())) {
throw new RuntimeException("Email exists");
}
User saved = userRepository.save(user);
emailService.send(user.getEmail(), "Welcome!");
return saved;
}
}
✅ Good:
@RestController
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@PostMapping("/users")
public ResponseEntity<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) {
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
private PaymentService paymentService;
public void process() {
if (this.total > 1000) {
paymentService.processLargeOrder(this);
} else {
paymentService.processSmallOrder(this);
}
}
}
✅ Good:
@Entity
public class Order {
private Long id;
private BigDecimal total;
private OrderStatus status;
}
@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
Component Structure
REST API Design
Data Access
Configuration
Exception Handling
Security
Testing
Performance
Code Quality
Related Skills
- uncle-duke-java: Java code review agent that uses this skill as reference
References
Official Documentation
Best Practices Guides
Version: 1.0
Last Updated: 2025-12-24
Maintainer: Development Team