Spring Boot skill for Claude Code

Typed config, honest JPA and fast test slices. Claude writes Spring Boot code with constructor injection, validated configuration records, and explicit JPA fetch plans instead of relying on open-session-in-view. Errors come back as ProblemDetail, tests use the narrowest slice with Testcontainers rather than H2, and Actuator exposes health probes and metrics without leaking env or heap dumps.

  • namespring-boot
  • size178 lines · ≈1,950 tokens
  • files1
  • licenseMIT
  • updated

What Claude sees before loading it

This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.

description: Builds and reviews Spring Boot 3.x services (with notes for Boot 4) - typed @ConfigurationProperties, profiles, Spring Data JPA without N+1 or open-session-in-view traps, transactions, Bean Validation, ProblemDetail error handling, test slices with Testcontainers, Actuator health and metrics, virtual threads, and graceful shutdown. Use when writing Spring controllers, services or repositories, fixing slow JPA queries or LazyInitializationException, configuring Actuator, or speeding up a slow Spring test suite.

Try asking

  • Fix the LazyInitializationException in our order endpoint properly
  • Set up Actuator health probes for Kubernetes on this Spring app
  • Our Spring test suite takes 12 minutes. Make it faster

Use it when

  • Writing Spring controllers, services or Spring Data repositories
  • JPA queries are slow or throw lazy-loading exceptions
  • Configuring profiles, properties, Actuator or shutdown
  • Restructuring a slow or flaky Spring test suite

Skip it when

How to check the result

The build passes with slice and Testcontainers tests, SQL logs show the expected statement count per endpoint, and readiness is UP while sensitive Actuator endpoints return 404.

The common mistake it prevents

Turning open-in-view back on to silence LazyInitializationException, which hides N+1 queries and holds connections for the whole request.

~/.claude/skills/spring-boot/SKILL.md

--- name: spring-boot description: Builds and reviews Spring Boot 3.x services (with notes for Boot 4) - typed @ConfigurationProperties, profiles, Spring Data JPA without N+1… license: MIT ---

A healthy Spring Boot service binds configuration into validated, typed objects, keeps JPA access inside explicit transactions with fetch plans chosen per use case, returns RFC 9457 ProblemDetail errors, tests each layer with the narrowest slice, and exposes only safe Actuator endpoints.

Workflow

- [ ] 1. Read build file: Boot version, Java version, starters, test deps
- [ ] 2. Read application.yml/properties per profile and the main config classes
- [ ] 3. Implement: controller (HTTP only) -> service (@Transactional) -> repository
- [ ] 4. Choose fetch strategy per query; check generated SQL
- [ ] 5. Validation + @RestControllerAdvice with ProblemDetail
- [ ] 6. Tests: slices for web/JPA, @SpringBootTest + Testcontainers for flows
- [ ] 7. Actuator, shutdown, and production profile review

Versions

Boot 3.x requires Java 17+ and Jakarta EE (jakarta.* imports). Boot 3.2 added virtual threads (spring.threads.virtual.enabled=true on Java 21), RestClient, and JdbcClient. Boot 3.4 made graceful shutdown the default and deprecated @MockBean in favor of @MockitoBean/@MockitoSpyBean (Spring Framework 6.2). Boot 4.0 (Nov 2025) moves to Spring Framework 7, Jakarta EE 11, and Jackson 3, and modularizes auto-configuration; when upgrading, first get to the latest 3.5.x with zero deprecation warnings, then follow the 4.0 migration guide.

Configuration

@ConfigurationProperties(prefix = "payments")
@Validated
public record PaymentsProperties(
    @NotBlank String apiBaseUrl,
    @NotNull Duration timeout,
    @Min(1) @Max(10) int maxRetries) {}

Enable with @ConfigurationPropertiesScan on the application class. Prefer this over scattered @Value. Profiles: application.yml holds defaults; application-prod.yml overrides; activate via SPRING_PROFILES_ACTIVE=prod. Secrets come from environment variables or a secrets backend, never committed YAML. Relaxed binding maps PAYMENTS_API_BASE_URL to payments.api-base-url.

JPA without the traps

Set these first in every service:

spring:
  jpa:
    open-in-view: false          # default true hides lazy loading in views and holds DB connections
    hibernate:
      ddl-auto: validate         # schema comes from Flyway/Liquibase, never Hibernate
    properties:
      hibernate.jdbc.batch_size: 50
      hibernate.order_inserts: true
ProblemFix
N+1 when serializing a collection@EntityGraph(attributePaths = {"items"}) on the repository method, or join fetch in JPQL
LazyInitializationExceptionFetch what the use case needs inside the transaction; map to DTOs before returning; never enable open-in-view to hide it
Paging + collection fetch join warning (HHH90003004)Page IDs first, then fetch entities with collections by ID list
Read-only queries@Transactional(readOnly = true) on the service; DTO projections (records) via JPQL constructor expressions or interface projections
Bulk updates@Modifying @Query("update ...") with clearAutomatically = true
Lost updates@Version field for optimistic locking; handle ObjectOptimisticLockingFailureException as 409
  • FetchType.EAGER on @ManyToOne is the default; set fetch = FetchType.LAZY on every @ManyToOne/@OneToOne and fetch explicitly.
  • Do not return entities from controllers; return records/DTOs. Entities with bidirectional relations recurse in Jackson and leak columns.
  • Log SQL during development: logging.level.org.hibernate.SQL=debug and count queries, or use a datasource proxy in tests to assert query counts.

Transactions

  • @Transactional on public service methods only. Self-invocation (this.other()) bypasses the proxy, so the annotation on other does nothing.
  • Checked exceptions do not roll back by default; use rollbackFor or unchecked exceptions.
  • Keep remote calls (HTTP, messaging) out of transactions; publish after commit with @TransactionalEventListener(phase = AFTER_COMMIT) or an outbox table.

Validation and errors

@RestController
@RequestMapping("/v1/orders")
class OrderController {
  private final OrderService service;
  OrderController(OrderService service) { this.service = service; }   // constructor injection only

  @PostMapping
  ResponseEntity<OrderResponse> create(@Valid @RequestBody CreateOrderRequest req, UriComponentsBuilder uri) {
    var order = service.create(req);
    return ResponseEntity.created(uri.path("/v1/orders/{id}").build(order.id())).body(order);
  }
}

@RestControllerAdvice
class ApiErrors extends ResponseEntityExceptionHandler {   // handles validation as ProblemDetail
  @ExceptionHandler(OrderNotFoundException.class)
  ProblemDetail notFound(OrderNotFoundException ex) {
    var pd = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
    pd.setType(URI.create("https://api.example.com/problems/order-not-found"));
    return pd;
  }
}

Set spring.mvc.problemdetails.enabled=true so framework exceptions render as application/problem+json. Validation failures return 400 by default; customize handleMethodArgumentNotValid to list field errors.

Test slices

Test targetAnnotationLoads
Controller mapping, validation, JSON@WebMvcTest(OrderController.class) + MockMvc (or MockMvcTester in 6.2+)Web layer only; mock services with @MockitoBean
Repository queries@DataJpaTest + Testcontainers PostgresJPA, Flyway, no web
JSON mapping@JsonTestObjectMapper only
Full flow@SpringBootTest(webEnvironment = RANDOM_PORT)Everything
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
class OrderRepositoryTest {
  @Container @ServiceConnection
  static PostgreSQLContainer<?> pg = new PostgreSQLContainer<>("postgres:17");
}

@ServiceConnection (Boot 3.1+) wires container URLs without @DynamicPropertySource. Avoid H2 as a stand-in for Postgres; dialect differences hide real bugs. Every distinct combination of @MockitoBeans creates a new application context; keep mocks consistent across tests to reuse cached contexts and keep the suite fast.

Actuator and runtime

management:
  endpoints.web.exposure.include: health,info,prometheus
  endpoint.health.probes.enabled: true       # /actuator/health/liveness and /readiness
  endpoint.health.show-details: when-authorized
server:
  shutdown: graceful                          # default since 3.4
spring.lifecycle.timeout-per-shutdown-phase: 25s

Never expose env, heapdump, threaddump, loggers publicly; put management on a separate port (management.server.port) not routed by the ingress. Liveness must not check downstream dependencies (a DB outage would restart every pod); readiness may.

Gotchas

  • Field injection (@Autowired on fields) hides dependencies and breaks tests; use constructors.
  • @Async and @Cacheable share the self-invocation proxy trap with @Transactional.
  • spring.jpa.hibernate.ddl-auto=update in production silently diverges schemas.
  • Virtual threads plus synchronized blocks around I/O pin carrier threads on Java 21-23; Java 24 removed most pinning. Size DB pools explicitly; virtual threads do not raise database capacity.
  • Lombok @Data on entities breaks equals/hashCode with lazy proxies; write them by ID.

Verification

  1. ./mvnw verify or ./gradlew check passes with slice and integration tests.
  2. SQL log for each touched endpoint shows the expected number of statements.
  3. curl localhost:8080/actuator/health/readiness returns UP; sensitive endpoints 404.
  4. Send SIGTERM during a request: it completes, and the app logs graceful shutdown.

About this skill

Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.

esc