
Spring Boot
- 205 installs
- 63 repo stars
- Updated July 18, 2026
- bobmatnyc/claude-mpm-skills
Scaffold and extend Spring Boot REST services with correct project structure, dependency injection, persistence, validation, and production-ready configuration patterns.
About
Teaches Claude Spring Boot backend development: bootstrapping apps, structuring layers, building REST APIs, integrating databases and messaging, applying Spring Security, and following JVM deployment conventions for maintainable Java services.
- Spring Boot project and module layout
- REST controllers and DTO validation
- JPA/data access and transaction patterns
- Security, config profiles, and observability hooks
- Testing and packaging for JVM services
Spring Boot by the numbers
- 205 all-time installs (skills.sh)
- Ranked #26 of 89 Java & JVM skills by installs in the Skillselion catalog
- Data as of Aug 1, 2026 (Skillselion catalog sync)
npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill spring-bootAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 205 |
|---|---|
| repo stars | ★ 63 |
| Last updated | July 18, 2026 |
| Repository | bobmatnyc/claude-mpm-skills ↗ |
What it does
Scaffold and extend Spring Boot REST services with correct project structure, dependency injection, persistence, validation, and production-ready configuration patterns.
Files
Spring Boot 3.x - Production-Ready Java Framework
Overview
Spring Boot is an opinionated Java framework for building production-ready applications with minimal configuration. It provides auto-configuration, embedded servers, and production-ready features like health checks and metrics.
Key Features:
- Auto-configuration (sensible defaults)
- Embedded servers (Tomcat, Jetty, Undertow)
- Dependency Injection with @Autowired
- Spring Data JPA for database access
- Spring Security for authentication/authorization
- Actuator for production monitoring
- Built-in testing support
Requirements:
- Java 17+ (Spring Boot 3.x requires Java 17 minimum)
- Maven or Gradle
Quick Start:
# Create project from Spring Initializr
curl https://start.spring.io/starter.zip \
-d type=maven-project \
-d language=java \
-d bootVersion=3.2.0 \
-d dependencies=web,data-jpa,postgresql,lombok,actuator \
-d name=myapp \
-o myapp.zip && unzip myapp.zip
# Run the application
cd myapp
./mvnw spring-boot:runProject Structure
src/
├── main/
│ ├── java/com/example/myapp/
│ │ ├── MyappApplication.java # Main class
│ │ ├── config/ # @Configuration classes
│ │ ├── controller/ # @RestController classes
│ │ ├── service/ # @Service classes
│ │ ├── repository/ # @Repository interfaces
│ │ ├── model/ # Entity classes
│ │ ├── dto/ # Data Transfer Objects
│ │ └── exception/ # Exception handlers
│ └── resources/
│ ├── application.yml # Configuration
│ └── application-{profile}.yml # Profile-specific config
└── test/
└── java/com/example/myapp/ # Test classesCore Annotations
Application Setup
// Main application class
@SpringBootApplication // Combines @Configuration, @EnableAutoConfiguration, @ComponentScan
public class MyappApplication {
public static void main(String[] args) {
SpringApplication.run(MyappApplication.class, args);
}
}Dependency Injection
// Constructor injection (recommended)
@Service
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
// @Autowired optional on single constructor (Spring 4.3+)
public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
}
// With Lombok
@Service
@RequiredArgsConstructor // Generates constructor for final fields
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
}
// Field injection (avoid in production code)
@Service
public class UserService {
@Autowired
private UserRepository userRepository; // Harder to test
}Component Stereotypes
@Component // Generic component
@Service // Business logic layer
@Repository // Data access layer (enables exception translation)
@Controller // MVC controller (returns views)
@RestController // REST API controller (returns JSON)
@Configuration // Configuration class with @Bean methodsREST Controllers
Basic Controller
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
// GET /api/v1/users
@GetMapping
public ResponseEntity<List<UserDto>> getAllUsers() {
return ResponseEntity.ok(userService.findAll());
}
// GET /api/v1/users/{id}
@GetMapping("/{id}")
public ResponseEntity<UserDto> getUserById(@PathVariable Long id) {
return userService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
// POST /api/v1/users
@PostMapping
public ResponseEntity<UserDto> createUser(@Valid @RequestBody CreateUserRequest request) {
UserDto created = userService.create(request);
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(created.getId())
.toUri();
return ResponseEntity.created(location).body(created);
}
// PUT /api/v1/users/{id}
@PutMapping("/{id}")
public ResponseEntity<UserDto> updateUser(
@PathVariable Long id,
@Valid @RequestBody UpdateUserRequest request) {
return ResponseEntity.ok(userService.update(id, request));
}
// DELETE /api/v1/users/{id}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.delete(id);
return ResponseEntity.noContent().build();
}
// GET /api/v1/users/search?email=test@example.com
@GetMapping("/search")
public ResponseEntity<List<UserDto>> searchUsers(
@RequestParam(required = false) String email,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
return ResponseEntity.ok(userService.search(email, page, size));
}
}Request/Response DTOs
// Request DTO with validation
@Data
public class CreateUserRequest {
@NotBlank(message = "Email is required")
@Email(message = "Invalid email format")
private String email;
@NotBlank(message = "Name is required")
@Size(min = 2, max = 100, message = "Name must be 2-100 characters")
private String name;
@NotBlank(message = "Password is required")
@Size(min = 8, message = "Password must be at least 8 characters")
private String password;
}
// Response DTO
@Data
@Builder
public class UserDto {
private Long id;
private String email;
private String name;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
public static UserDto fromEntity(User user) {
return UserDto.builder()
.id(user.getId())
.email(user.getEmail())
.name(user.getName())
.createdAt(user.getCreatedAt())
.updatedAt(user.getUpdatedAt())
.build();
}
}Service Layer
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true) // Default to read-only transactions
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public List<UserDto> findAll() {
return userRepository.findAll().stream()
.map(UserDto::fromEntity)
.collect(Collectors.toList());
}
public Optional<UserDto> findById(Long id) {
return userRepository.findById(id)
.map(UserDto::fromEntity);
}
@Transactional // Read-write transaction
public UserDto create(CreateUserRequest request) {
if (userRepository.existsByEmail(request.getEmail())) {
throw new EmailAlreadyExistsException(request.getEmail());
}
User user = User.builder()
.email(request.getEmail())
.name(request.getName())
.passwordHash(passwordEncoder.encode(request.getPassword()))
.build();
return UserDto.fromEntity(userRepository.save(user));
}
@Transactional
public UserDto update(Long id, UpdateUserRequest request) {
User user = userRepository.findById(id)
.orElseThrow(() -> new UserNotFoundException(id));
if (request.getName() != null) {
user.setName(request.getName());
}
if (request.getEmail() != null) {
user.setEmail(request.getEmail());
}
return UserDto.fromEntity(userRepository.save(user));
}
@Transactional
public void delete(Long id) {
if (!userRepository.existsById(id)) {
throw new UserNotFoundException(id);
}
userRepository.deleteById(id);
}
public List<UserDto> search(String email, int page, int size) {
Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
Page<User> users = email != null
? userRepository.findByEmailContainingIgnoreCase(email, pageable)
: userRepository.findAll(pageable);
return users.stream()
.map(UserDto::fromEntity)
.collect(Collectors.toList());
}
}Repository Layer (Spring Data JPA)
Basic Repository
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// Derived query methods
Optional<User> findByEmail(String email);
boolean existsByEmail(String email);
List<User> findByNameContainingIgnoreCase(String name);
// Paginated queries
Page<User> findByEmailContainingIgnoreCase(String email, Pageable pageable);
// Custom JPQL query
@Query("SELECT u FROM User u WHERE u.createdAt > :date AND u.active = true")
List<User> findActiveUsersCreatedAfter(@Param("date") LocalDateTime date);
// Native SQL query
@Query(value = "SELECT * FROM users WHERE email ILIKE %:email%", nativeQuery = true)
List<User> searchByEmail(@Param("email") String email);
// Modifying query
@Modifying
@Query("UPDATE User u SET u.active = false WHERE u.lastLoginAt < :date")
int deactivateInactiveUsers(@Param("date") LocalDateTime date);
}Entity Class
@Entity
@Table(name = "users")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String name;
@Column(name = "password_hash", nullable = false)
private String passwordHash;
@Column(nullable = false)
@Builder.Default
private boolean active = true;
@Column(name = "created_at", nullable = false, updatable = false)
@CreationTimestamp
private LocalDateTime createdAt;
@Column(name = "updated_at", nullable = false)
@UpdateTimestamp
private LocalDateTime updatedAt;
// Relationships
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
@Builder.Default
private List<Post> posts = new ArrayList<>();
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(
name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
@Builder.Default
private Set<Role> roles = new HashSet<>();
}Configuration
application.yml
spring:
application:
name: myapp
datasource:
url: jdbc:postgresql://localhost:5432/mydb
username: ${DB_USERNAME:postgres}
password: ${DB_PASSWORD:password}
hikari:
maximum-pool-size: 10
minimum-idle: 5
connection-timeout: 30000
jpa:
hibernate:
ddl-auto: validate # none, validate, update, create, create-drop
show-sql: false
properties:
hibernate:
format_sql: true
default_schema: public
profiles:
active: ${SPRING_PROFILES_ACTIVE:dev}
server:
port: ${PORT:8080}
servlet:
context-path: /api
# Actuator endpoints
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: when_authorized
# Custom properties
app:
jwt:
secret: ${JWT_SECRET:your-secret-key}
expiration-ms: 86400000Profile-Specific Configuration
# application-dev.yml
spring:
jpa:
show-sql: true
h2:
console:
enabled: true
logging:
level:
com.example.myapp: DEBUG
org.springframework.web: DEBUG
---
# application-prod.yml
spring:
jpa:
show-sql: false
properties:
hibernate:
generate_statistics: false
logging:
level:
com.example.myapp: INFO
org.springframework.web: WARNConfiguration Properties Class
@Configuration
@ConfigurationProperties(prefix = "app.jwt")
@Data
public class JwtProperties {
private String secret;
private long expirationMs;
}
// Usage
@Service
@RequiredArgsConstructor
public class JwtService {
private final JwtProperties jwtProperties;
public String generateToken(User user) {
return Jwts.builder()
.setSubject(user.getEmail())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + jwtProperties.getExpirationMs()))
.signWith(Keys.hmacShaKeyFor(jwtProperties.getSecret().getBytes()))
.compact();
}
}Exception Handling
Global Exception Handler
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// Handle validation errors
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationErrors(MethodArgumentNotValidException ex) {
List<String> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.toList());
ErrorResponse response = ErrorResponse.builder()
.status(HttpStatus.BAD_REQUEST.value())
.message("Validation failed")
.errors(errors)
.timestamp(LocalDateTime.now())
.build();
return ResponseEntity.badRequest().body(response);
}
// Handle not found exceptions
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
ErrorResponse response = ErrorResponse.builder()
.status(HttpStatus.NOT_FOUND.value())
.message(ex.getMessage())
.timestamp(LocalDateTime.now())
.build();
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
}
// Handle business logic exceptions
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException ex) {
ErrorResponse response = ErrorResponse.builder()
.status(HttpStatus.CONFLICT.value())
.message(ex.getMessage())
.timestamp(LocalDateTime.now())
.build();
return ResponseEntity.status(HttpStatus.CONFLICT).body(response);
}
// Catch-all handler
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleAllExceptions(Exception ex) {
log.error("Unexpected error occurred", ex);
ErrorResponse response = ErrorResponse.builder()
.status(HttpStatus.INTERNAL_SERVER_ERROR.value())
.message("An unexpected error occurred")
.timestamp(LocalDateTime.now())
.build();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
}
}
// Error response DTO
@Data
@Builder
public class ErrorResponse {
private int status;
private String message;
private List<String> errors;
private LocalDateTime timestamp;
}
// Custom exceptions
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String resource, Long id) {
super(String.format("%s not found with id: %d", resource, id));
}
}
public class UserNotFoundException extends ResourceNotFoundException {
public UserNotFoundException(Long id) {
super("User", id);
}
}Spring Security
Security Configuration (Spring Security 6.x)
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthFilter;
private final UserDetailsService userDetailsService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/auth/**").permitAll()
.requestMatchers("/actuator/health").permitAll()
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
.anyRequest().authenticated())
.authenticationProvider(authenticationProvider())
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder());
return provider;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config)
throws Exception {
return config.getAuthenticationManager();
}
}JWT Filter
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
final String jwt = authHeader.substring(7);
final String userEmail = jwtService.extractUsername(jwt);
if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(userEmail);
if (jwtService.isTokenValid(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
}Actuator Endpoints
# Built-in endpoints
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus,env
base-path: /actuator
endpoint:
health:
show-details: when_authorized
probes:
enabled: true # Kubernetes liveness/readiness probes
info:
env:
enabled: true
# Application info
info:
app:
name: ${spring.application.name}
version: '@project.version@'
java:
version: ${java.version}Common Actuator Endpoints:
GET /actuator/health- Application healthGET /actuator/health/liveness- Kubernetes liveness probeGET /actuator/health/readiness- Kubernetes readiness probeGET /actuator/info- Application informationGET /actuator/metrics- Metrics listGET /actuator/metrics/{name}- Specific metricGET /actuator/prometheus- Prometheus format metrics
Testing
Unit Testing Controllers
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Autowired
private ObjectMapper objectMapper;
@Test
void shouldReturnUserById() throws Exception {
UserDto user = UserDto.builder()
.id(1L)
.email("test@example.com")
.name("Test User")
.build();
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"));
}
@Test
void shouldReturn404WhenUserNotFound() throws Exception {
when(userService.findById(999L)).thenReturn(Optional.empty());
mockMvc.perform(get("/api/v1/users/999"))
.andExpect(status().isNotFound());
}
@Test
void shouldCreateUser() throws Exception {
CreateUserRequest request = new CreateUserRequest();
request.setEmail("new@example.com");
request.setName("New User");
request.setPassword("password123");
UserDto created = UserDto.builder()
.id(1L)
.email("new@example.com")
.name("New User")
.build();
when(userService.create(any())).thenReturn(created);
mockMvc.perform(post("/api/v1/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.email").value("new@example.com"));
}
}Integration Testing
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY)
@Transactional
class UserIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private UserRepository userRepository;
@Test
void shouldCreateAndRetrieveUser() {
CreateUserRequest request = new CreateUserRequest();
request.setEmail("integration@test.com");
request.setName("Integration Test");
request.setPassword("password123");
ResponseEntity<UserDto> createResponse = restTemplate.postForEntity(
"/api/v1/users", request, UserDto.class);
assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(createResponse.getBody()).isNotNull();
assertThat(createResponse.getBody().getEmail()).isEqualTo("integration@test.com");
Long userId = createResponse.getBody().getId();
ResponseEntity<UserDto> getResponse = restTemplate.getForEntity(
"/api/v1/users/" + userId, UserDto.class);
assertThat(getResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(getResponse.getBody().getName()).isEqualTo("Integration Test");
}
}Repository Testing
@DataJpaTest
class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Autowired
private TestEntityManager entityManager;
@Test
void shouldFindByEmail() {
User user = User.builder()
.email("test@example.com")
.name("Test")
.passwordHash("hash")
.build();
entityManager.persistAndFlush(user);
Optional<User> found = userRepository.findByEmail("test@example.com");
assertThat(found).isPresent();
assertThat(found.get().getName()).isEqualTo("Test");
}
}Best Practices
1. Use Constructor Injection
// Prefer constructor injection with final fields
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository; // final = immutable
}2. Layer Separation
// Controller -> Service -> Repository
// DTOs for API layer, Entities for persistence layer
// Never expose entities directly in REST responses3. Transaction Management
@Service
@Transactional(readOnly = true) // Default read-only
public class UserService {
@Transactional // Write transaction
public void updateUser() { }
}4. Configuration Externalization
# Use environment variables for secrets
spring:
datasource:
password: ${DB_PASSWORD} # From environment5. Error Handling
// Use @RestControllerAdvice for global exception handling
// Return consistent error responses
// Never expose internal details in productionCode Quality & Robustness Anti-Patterns
Beyond framework patterns, watch for a recurring set of robustness and changeability defects in services, controllers, and exception handlers. These are caught by static analysis but are easy to introduce by hand:
- Generic `catch (Exception)` buried in a method — catch the specific types you can
handle. A broad catch is correct only at the top-level boundary (e.g. the @RestControllerAdvice catch-all), commented as intentional.
- Catch clauses that only rethrow the same exception — delete them or have them add
context (wrap into a typed domain exception).
- Throwing raw `RuntimeException`/`Exception` — throw typed domain exceptions so
@ExceptionHandler can map them to the right HTTP status.
- Nested `try`/`catch` — extract the inner concern into its own method.
- `if … else if` with no final `else` and `switch` with no `default` — handle the
residual case or document why none is needed (defensive programming).
- Switch fall-through from non-empty cases and nested switches — prefer the
arrow switch (Java 14+) over enums/sealed types for compiler-enforced exhaustiveness.
- Collapsible nested `if` and loop-counter modification inside the loop body —
combine conditions; never reassign a for counter in its body.
See [Quality Anti-Patterns](references/quality-antipatterns.md) for each defect with compliant/non-compliant Java examples, severities, and false-positive filters. Patterns derived from CAST Highlight code quality indicators (https://doc.casthighlight.com/).
Resources
- Spring Boot Documentation: https://docs.spring.io/spring-boot/docs/current/reference/html/
- Spring Data JPA: https://docs.spring.io/spring-data/jpa/docs/current/reference/html/
- Spring Security: https://docs.spring.io/spring-security/reference/
- Spring Initializr: https://start.spring.io/
- Baeldung Tutorials: https://www.baeldung.com/spring-boot
Related Skills
When using Spring Boot, consider these complementary skills:
- mongodb: NoSQL database integration with Spring Data MongoDB
- docker: Containerizing Spring Boot applications
- kubernetes: Deploying Spring Boot microservices
- postgresql: Relational database patterns with JPA
{
"name": "spring-boot",
"version": "1.1.0",
"updated": "2026-06-15",
"category": "toolchain",
"toolchain": "java",
"framework": "spring-boot",
"tags": [
"java",
"spring-boot",
"spring",
"microservices",
"rest-api",
"dependency-injection",
"jpa",
"security",
"actuator"
],
"entry_point_tokens": 85,
"full_tokens": 8701,
"related_skills": [
"../../../databases/mongodb"
],
"author": "Claude MPM Team",
"license": "MIT"
}
Java Robustness & Changeability Anti-Patterns
A focused set of code-quality defects that recur in Spring Boot services, controllers, and exception handlers. These are robustness (correctness/reliability) and changeability (readability/maintainability) issues — they are orthogonal to the framework patterns (DI, REST, Spring Data) covered in the main skill. Most are caught by a good static analyzer or a careful reviewer; the value here is knowing why each matters and what the compliant shape looks like in idiomatic Java.
Source note: The defect families below are derived from CAST Highlight code
quality indicators (https://doc.casthighlight.com/), cross-referenced with the
primary sources CAST itself cites (SonarSource RSPEC rules, the SEI CERT Java
guidelines, and MISRA). Patterns are paraphrased with original Spring-flavored
examples; severities are review guidance, not CAST's proprietary calibration.
This is a statistical lens: a single occurrence is rarely fatal, but a high density of these across a service signals reliability and maintenance risk. Apply an ~80% confidence filter — flag the clear cases, ask a question on the borderline ones.
---
1. Nested try/catch blocks
Family: Robustness — Severity: MEDIUM
A try block containing another try/catch makes it hard to reason about which handler catches which failure, and it usually means two unrelated concerns have been crammed into one method.
Non-compliant:
public Report build(Long userId) {
try {
User user = userRepository.findById(userId).orElseThrow();
if (user.isActive()) {
try { // nested try — VIOLATION
return reportClient.fetch(user);
} catch (IOException e) {
throw new ReportUnavailableException(userId, e);
}
}
return Report.empty();
} catch (Exception e) {
throw new ReportUnavailableException(userId, e);
}
}Compliant — extract the inner concern into its own method:
public Report build(Long userId) {
User user = userRepository.findById(userId).orElseThrow();
return user.isActive() ? fetchReport(user) : Report.empty();
}
private Report fetchReport(User user) {
try {
return reportClient.fetch(user);
} catch (IOException e) {
throw new ReportUnavailableException(user.getId(), e);
}
}False-positive filter: Two sequential (not nested) try blocks are fine. A single try-with-resources whose body calls a method that itself has a try is not "nested" at the syntactic level worth flagging — focus on a try lexically inside another try.
---
2. Overly generic catch (Exception ...) / catch (Throwable ...)
Family: Robustness — Severity: MEDIUM (HIGH at a boundary that swallows)
Catching Exception or Throwable captures failures you never anticipated — NullPointerException, ClassCastException, even Error subtypes — and routes them through one handler. It hides the failure surface: when a called method later throws a new checked exception, the compiler will not remind you to handle it differently.
Non-compliant:
try {
parsePayload(body); // throws ParseException
validate(body); // throws ValidationException
persist(body); // throws DataAccessException
} catch (Exception e) { // VIOLATION — one bucket for everything
log.error("failed", e);
}Compliant — catch the specific types you can actually handle:
try {
parsePayload(body);
validate(body);
persist(body);
} catch (ParseException | ValidationException e) {
throw new BadRequestException(e.getMessage(), e);
} catch (DataAccessException e) {
throw new StorageException("persist failed", e);
}Legitimate exception: A top-level boundary deliberately catches broadly to keep a batch job or a request thread alive (this is exactly what Spring's @RestControllerAdvice catch-all handler does — see the main skill). That is acceptable when it is the outermost layer, logs the cause, and is commented as intentional. The same catch (Exception) buried in a service method is a violation.
---
3. Catch clauses that only rethrow
Family: Robustness — Severity: LOW (noise / misleading)
A catch whose only statement rethrows the same exception is equivalent to not catching at all — except it adds code and makes a reader stop to check whether something subtle is happening. Either remove the clause or give it real work (wrap, enrich, log-and-translate).
Non-compliant:
try {
return service.charge(order);
} catch (PaymentException e) {
throw e; // VIOLATION — adds nothing
}Compliant — either drop the try entirely:
return service.charge(order); // let PaymentException propagate…or make the catch earn its place by adding context:
try {
return service.charge(order);
} catch (PaymentException e) {
throw new OrderFailedException(order.getId(), e); // translation + context
}False-positive filter: A catch that rethrows a different type, or rethrows after logging / cleanup / metric increment, is not a violation — it is doing real work.
---
4. Throwing raw system exceptions
Family: Robustness — Severity: MEDIUM
Throwing java.lang.RuntimeException or java.lang.Error (or new Exception(...)) forces every caller into a generic catch (see #2) and conveys no semantic meaning. Applications should throw their own typed exceptions so callers can distinguish a not-found from a conflict from a validation failure — which is also how Spring's @ExceptionHandler routing decides the HTTP status.
Non-compliant:
public Account load(Long id) {
Account a = repo.findById(id).orElse(null);
if (a == null) {
throw new RuntimeException("no account " + id); // VIOLATION
}
return a;
}Compliant — a domain exception the advice layer maps to 404:
public Account load(Long id) {
return repo.findById(id)
.orElseThrow(() -> new AccountNotFoundException(id));
}False-positive filter: Framework-mandated unchecked throws and genuinely programmer-error guards (throw new IllegalArgumentException(...) for a contract violation) are idiomatic and not flagged — the target is opaque RuntimeException / Exception used as a lazy signal.
---
5. Collapsible nested if
Family: Changeability — Severity: LOW
Two nested if statements, neither with an else, where the inner if is the only statement in the outer, should be a single combined condition. Collapsing improves readability and reduces nesting depth.
Non-compliant:
if (file != null) {
if (file.isFile() || file.isDirectory()) { // VIOLATION — collapsible
process(file);
}
}Compliant:
if (file != null && (file.isFile() || file.isDirectory())) {
process(file);
}False-positive filter: Does not apply when the outer if has an else, when the inner if has an else, or when the inner if is not the sole statement of the outer (e.g., other statements follow it). Also do not collapse when an early-return guard (if (file == null) return;) reads more clearly than a compound condition — that is a different, often-preferable pattern.
---
6. if … else if without a final else
Family: Robustness — Severity: MEDIUM
A chain of if … else if that lacks a terminal else silently does nothing for any input the author did not enumerate. Defensive programming requires a final else that either handles the residual case or carries a comment stating why no action is correct. This mirrors the "switch needs a default" rule (#8).
Non-compliant:
if (status == Status.PENDING) {
enqueue(order);
} else if (status == Status.PAID) {
ship(order);
}
// unhandled: CANCELLED, REFUNDED, ... fall through silently — VIOLATIONCompliant:
if (status == Status.PENDING) {
enqueue(order);
} else if (status == Status.PAID) {
ship(order);
} else {
throw new IllegalStateException("unhandled order status: " + status);
}False-positive filter: When every branch ends in return/throw, the code after the chain already behaves as the implicit else, so a missing trailing else is acceptable. Modern Java often replaces the whole chain with an exhaustive switch expression over an enum/sealed type, which the compiler checks for completeness — prefer that where it fits.
---
7. Loop counter modified inside the loop body
Family: Robustness — Severity: MEDIUM
Reassigning a for loop's counter inside the body (beyond the loop's own update clause) makes iteration count hard to predict and is a classic source of off-by-N and skip/infinite-loop bugs. Logical control flags may be updated in the body; the counter should not be.
Non-compliant:
for (int i = 0; i < items.size(); i++) {
handle(items.get(i));
if (items.get(i).isBatchBoundary()) {
i += 2; // VIOLATION — counter mutated in body
}
}Compliant — model the skip explicitly, or use a higher-level construct:
for (int i = 0; i < items.size(); i++) {
Item item = items.get(i);
if (shouldSkip(item)) {
continue; // intent is explicit; counter untouched
}
handle(item);
}
// or, when no index is needed:
items.stream().filter(this::shouldProcess).forEach(this::handle);False-positive filter: Updating a separate control variable (a boolean done flag tested in the condition) is permitted. Genuinely index-driven algorithms that must advance irregularly (parsers, sliding windows) may need manual index control — prefer a while loop there so the manual advancement is not disguised as a for.
---
8. Switch hygiene: missing default and missing break
Family: Robustness — Severity: MEDIUM
Two related defects:
- No `default` clause — like a missing final
else(#6), unenumerated values pass
through unhandled. CWE-478 ("missing default case") links this to cascading failures.
- Fall-through from a non-empty case — a
casewith statements but no terminating
break/return/throw/continue falls into the next case, usually unintentionally.
Non-compliant:
switch (plan) {
case FREE:
applyFreeLimits(account);
// VIOLATION: no break — falls into PRO
case PRO:
applyProLimits(account);
break;
// VIOLATION: no default — ENTERPRISE silently unhandled
}Compliant — terminate every non-empty case and always provide a default:
switch (plan) {
case FREE -> applyFreeLimits(account); // arrow form: no fall-through possible
case PRO -> applyProLimits(account);
case ENTERPRISE -> applyEnterpriseLimits(account);
default -> throw new IllegalStateException("unknown plan: " + plan);
}Guidance: Prefer the arrow `switch` (Java 14+) — it cannot fall through and, over an enum or sealed type, the compiler enforces exhaustiveness so a default may even be unnecessary. Stacked labels sharing one body (case A: case B: doX(); break;) are empty intermediate cases and are not fall-through violations.
---
9. Nested switch statements
Family: Transferability — Severity: LOW
A switch inside another switch is easy to misread — a reader can mistake an inner case for an outer one. Extract the inner switch into a well-named method.
Non-compliant:
switch (region) {
case US:
switch (state) { // VIOLATION — nested switch
case CA: return californiaRate();
default: return usDefaultRate();
}
default:
return globalRate();
}Compliant:
switch (region) {
case US -> usStateRate(state); // inner switch lives in its own method
default -> globalRate();
}False-positive filter: A single, shallow nested switch over a small fixed set may be clearer inline than split. Flag it as a readability question, not a hard finding — weight it by nesting depth and case count.
---
How these affect a Spring Boot review
These items are predominantly MEDIUM/LOW and do not, by themselves, block a merge. Their value is as a density signal: a service class carrying several of them (generic catches + rethrow-only catches + raw RuntimeException throws) is a reliability hotspot worth refactoring before adding features. Two of them interlock with framework design: typed domain exceptions (#4) drive @RestControllerAdvice status mapping, and the deliberate top-level catch-all (#2 exception) is the one place a broad catch is correct. Record MEDIUM findings in the review handoff; fix LOW ones opportunistically while the code is already open.