About spring-boot
A development skill for Spring Boot 4.x projects, aimed at developers building production-oriented services. It provides guidance on clean architecture, security, testing, data access, and deployment, with concrete conventions and code patterns.
Key features
- 01Layered architecture and constructor injection
- 02REST validation and Problem Details errors
- 03Security and OAuth2/JWT guidance
- 04Flyway migrations and data access patterns
- 05Unit and integration testing workflows
- 06Actuator health-check configuration
Use cases
- 01Design a Spring Boot service before implementation
- 02Build validated REST endpoints and error handling
- 03Add security and database migrations
- 04Verify tests and health checks before deployment
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ piomin/ claude-ai-spring-boot --skill spring-bootAfter installing, type/spring-bootin Claude Code to confirm it loaded.
Bundled files
References and scripts installed along with SKILL.md.
references/cloud.mddata.mdsecurity.mdtesting.mdui-contract.mdweb.mdDetails
From the project READMESpring Boot Skill
Enterprise Spring Boot 4.x development with focus on clean architecture and production-ready code.
Core Workflow
- Analyze - Understand requirements, identify service boundaries, APIs, data models
- Design - Plan architecture, confirm design before coding
- Implement - Build with constructor injection and layered architecture; for a relational database, write Flyway migrations alongside entities (
ddl-auto: validate) - Secure - Add Spring Security, OAuth2, method security; verify tests pass
- Test - Write unit, integration tests; run
./mvnw clean verifyand confirm all pass, with integration tests running (not skipped) - Deploy - Configure health checks via Actuator; validate
/actuator/healthreturns UP and/actuator/infoshowsbuild.version
Quick Start Templates
Entity
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank
private String name;
@DecimalMin("0.0")
private BigDecimal price;
// Getters/Setters (no Lombok)
}
Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
List<Product> findByNameContainingIgnoreCase(String name);
}
Service
@Service
@Transactional(readOnly = true)
public class ProductService {
private final ProductRepository repo;
public ProductService(ProductRepository repo) {
this.repo = repo;
}
public List<ProductResponse> search(String name) {
return repo.findByNameContainingIgnoreCase(name).stream()
.map(ProductResponse::from)
.toList();
}
@Transactional
public ProductResponse create(ProductRequest request) {
var product = new Product();
product.setName(request.name());
product.setPrice(request.price());
return ProductResponse.from(repo.save(product));
}
}
REST Controller
@RestController
@RequestMapping("/api/v1/products")
@Validated
public class ProductController {
private final ProductService service;
public ProductController(ProductService service) {
this.service = service;
}
@GetMapping
public List<ProductResponse> search(@RequestParam(defaultValue = "") String name) {
return service.search(name);
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public ProductResponse create(@Valid @RequestBody ProductRequest request) {
return service.create(request);
}
}
DTOs (Records)
public record ProductRequest(
@NotBlank String name,
@DecimalMin("0.0") BigDecimal price
) {}
public record ProductResponse(Long id, String name, BigDecimal price) {
public static ProductResponse from(Product product) {
return new ProductResponse(product.getId(), product.getName(), product.getPrice());
}
}
Global Exception Handler (Problem Details)
@RestControllerAdvice
public class ApiExceptionHandler extends ResponseEntityExceptionHandler {
public record FieldMessage(String field, String code, String message) {}
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers,
HttpStatusCode status, WebRequest request) {
ex.getBody().setProperty("errors", ex.getFieldErrors().stream()
.map(e -> new FieldMessage(e.getField(), code(e.getCode()), e.getDefaultMessage()))
.toList());
return super.handleMethodArgumentNotValid(ex, headers, status, request);
}
private static String code(String constraint) {
return switch (constraint) {
case "NotBlank", "NotNull" -> "required";
case null, default -> "invalid";
};
}
@ExceptionHandler
ProblemDetail handleOptimisticLock(ObjectOptimisticLockingFailureException ex) {
return ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, "Record was changed by someone else; reload and retry");
}
@ExceptionHandler
ProblemDetail handleUnexpected(Exception ex) {
logger.error("Unexpected error", ex);
return ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, "Unexpected server error");
}
}
Every error is RFC 9457 Problem Details with field errors in errors: [{field, code, message}]. Full handler (unique constraints, bad sort, documented codes): references/web.md.
Test Slice
@WebMvcTest(ProductController.class)
class ProductControllerTest {
@Autowired MockMvc mockMvc;
@MockitoBean ProductService service;
@Test
void createProduct_validRequest_returns201() throws Exception {
var response = new ProductResponse(1L, "Widget", new BigDecimal("10.00"));
when(service.create(any())).thenReturn(response);
mockMvc.perform(post("/api/v1/products")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"name":"Widget","price":10.0}
"""))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.name").value("Widget"));
}
}
Reference Guide
Load detailed patterns based on context:
| Topic | Reference | When to Load |
|---|---|---|
| Web/REST | references/web.md | Controllers, paged lists, validation, PUT and version, Problem Details (RFC 9457) with errors[] |
| API built for a UI | references/ui-contract.md | Only when a UI or generated client consumes the OpenAPI contract (see conventions): /api, springdoc 3 contract export, error codes, filters, relations, Keycloak, "Connect a UI" |
| Data Access | references/data.md | JPA, repositories, transactions, queries, Flyway migrations |
| Security | references/security.md | Spring Security 6, OAuth2, JWT, auth |
| Cloud/Config | references/cloud.md | Config server, discovery, resilience, virtual threads, HTTP interfaces, GraalVM native, OTLP tracing |
| Testing | references/testing.md | Unit, integration, slice tests, Testcontainers @ServiceConnection |
Constraints
MUST DO
- Constructor injection (no field injection)
@Validon all request bodies@Transactionalfor multi-step writes@Transactional(readOnly = true)for reads- Type-safe config with
@ConfigurationProperties - Global exception handling with
@RestControllerAdvice - Externalize secrets (use env vars, not properties files)
- Flyway migrations +
ddl-auto: validatewhen persisting to a relational database (skip Flyway otherwise) - Errors as Problem Details; list endpoints return
PagedModel - Permit
/actuator/health/**so the liveness and readiness probes work behind security - Only when the API is built for a UI (see
conventions, "REST API Built for a UI"): followreferences/ui-contract.md
MUST NOT DO
- Field injection (
@Autowiredon fields) - Skip input validation on endpoints
- Mix blocking and reactive code
- Store secrets in application.properties
- Use deprecated Spring Boot 2.x/3.x patterns
- Hardcode URLs, credentials, environment values
- Use
ddl-auto: update/createoutside tests, or edit an already-applied migration - Return
Page/PageImplfrom a controller, or put@EnableSpringDataWebSupporton the application class (@WebMvcTestregisters it twice) - Put
@ApiResponseon controller methods (springdoc 3 then drops the success response; use anOpenApiCustomizer) - Return an exception's message from the catch-all handler
Spring Boot 4 / Spring Data 4 Notes
PropertyReferenceExceptionandTypeInformationmoved toorg.springframework.data.core@MockBean/@SpyBeanare removed: use@MockitoBean/@MockitoSpyBean@WebMvcTestcreates no repositories: every repository a controller injects needs a@MockitoBeanin its test- Build with
./mvnw clean verify: classes compiled by the IDE intargetcan hidejavacerrors
Architecture Patterns
Project Structure:
src/main/java/pl/piomin/services/
├── controller/ # REST endpoints
├── service/ # Business logic
├── repository/ # Data access
├── model/ # Entities
├── dto/ # Request/Response DTOs
├── config/ # Configuration
└── exception/ # Custom exceptions + handler
Layering:
- Controller → Service → Repository
- Controller handles HTTP, validation
- Service handles business logic, transactions
- Repository handles data persistence
Clean Architecture Principles:
- Domain models independent of frameworks
- Use case driven design
- Dependency inversion (interfaces)
- Clear boundaries between layers
Common Annotations
| Annotation | Purpose |
|---|---|
@RestController | REST controller (combines @Controller + @ResponseBody) |
@Service | Business logic component |
@Repository | Data access component |
@Transactional | Transaction management |
@Valid | Trigger validation |
@ConfigurationProperties | Bind properties to class |
@EnableMethodSecurity | Enable method security |
Reactive WebFlux Endpoint
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@GetMapping("/{id}")
public Mono<ResponseEntity<OrderDto>> getOrder(@PathVariable UUID id) {
return orderService.findById(id)
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Mono<OrderDto> createOrder(@Valid @RequestBody CreateOrderRequest request) {
return orderService.create(request);
}
}
Spring Security JWT
@Configuration
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(s -> s.sessionCreationPolicy(STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health/**").permitAll() // + "/actuator/info" when built for a UI
.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
.build();
}
}
Knowledge Base
Spring Boot 4.x, Java 21+, Spring WebFlux, Project Reactor, Spring Data JPA, Spring Security, OAuth2/JWT, Hibernate, R2DBC, Spring Cloud, Resilience4j, Micrometer, JUnit 5, Testcontainers, Mockito, Maven, Paketo Buildpacks, Skaffold