Spring Boot skill for Codex

Typed config, thin controllers, fast test slices. Codex builds Spring Boot features the way an experienced Java team does: records for configuration that fail fast at startup, DTOs instead of entities on the wire, transactions on services, open-in-view switched off and lazy loading fixed with entity graphs. Tests use the narrowest slice that proves the change, with @ServiceConnection containers instead of hand-wired URLs.

  • Backend & APIs
  • $spring-boot
  • 208 lines
  • 2 files
  • ~2,100 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- spring-boot

Installs to ~/.agents/skills/spring-boot/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $spring-boot, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Builds and fixes Spring Boot 3 services - typed @ConfigurationProperties, profiles, Spring MVC controllers with validation and ProblemDetail errors, Spring Data JPA with Flyway, Actuator health probes, virtual threads, and test slices (@WebMvcTest, @DataJpaTest, Testcontainers @ServiceConnection). Use when adding an endpoint or entity to a Spring Boot app, wiring configuration, fixing LazyInitializationException or N+1 queries, speeding up a slow test suite, or upgrading between Boot 3.x releases.

Use it when

  • Adding endpoints, entities or configuration to a Spring Boot 3 service
  • JPA throws lazy-loading errors or a list endpoint runs N+1 queries
  • Tests boot a full context everywhere and the suite is slow
  • Moving between Boot 3.x minor releases and handling deprecations

Use something else when

  • The service is Kotlin on Ktor rather than Spring: use ktor-server
  • The problem is JVM heap or GC tuning rather than application code: use jvm-tuning

How to check the result

Run ./mvnw verify or ./gradlew check, then start the app and call the new endpoint once, confirming the status code, body and a problem-details response for a bad request.

The mistake it prevents

Re-enabling open-in-view or returning JPA entities from controllers to make lazy-loading errors disappear, which hides N+1 queries until production load.

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

--- name: spring-boot description: Builds and fixes Spring Boot 3 services - typed @ConfigurationProperties, profiles, Spring MVC controllers with validation and ProblemDetail errors,… license: MIT ---

Done well means: configuration is typed and validated at startup, controllers are thin and return RFC 9457 problem details on failure, transactions sit on the service layer, JPA never lazy-loads outside a transaction, and each layer has a test slice that boots in seconds rather than a full context per test class.

Before changing anything

  1. Read AGENTS.md, then the build file. Note the build tool (./mvnw or ./gradlew; always use the wrapper), the Boot version, the Java toolchain, and which starters exist.
  2. Check the Boot line: 3.2+ has RestClient and virtual threads, 3.4+ has @MockitoBean and MockMvcTester. Boot 4 (Spring Framework 7, Jackson 3) changes package names and starters; do not mix Boot 4 advice into a 3.x project.
  3. Read src/main/resources/application.yml (or .properties) and any profile files.
  4. Run the tests once before editing: ./mvnw -q test or ./gradlew test. Dependency resolution needs network; in an offline sandbox, use what is in ~/.m2 or ~/.gradle and ask before requesting network access.

Project layout

Package by feature, not by layer, once a service has more than a handful of entities:

com.acme.orders/
  OrdersApplication.java
  order/        OrderController, OrderService, OrderRepository, Order, OrderDto
  customer/
  config/       AppProperties, SecurityConfig, ClockConfig

Keep the @SpringBootApplication class at the root package so component scanning finds every feature package.

Configuration

Use records with @ConfigurationProperties and validate them so a bad deploy fails at boot, not on the first request:

@Validated
@ConfigurationProperties(prefix = "app.payments")
public record PaymentProperties(
    @NotNull URI baseUrl,
    @NotNull Duration timeout,
    @Min(1) @Max(10) int maxRetries) {}
@SpringBootApplication
@ConfigurationPropertiesScan
public class OrdersApplication { public static void main(String[] a) { SpringApplication.run(OrdersApplication.class, a); } }
app:
  payments:
    base-url: https://payments.internal
    timeout: 3s
    max-retries: 3
spring:
  jpa:
    open-in-view: false
  threads:
    virtual:
      enabled: true          # Java 21+, Boot 3.2+
  mvc:
    problemdetails:
      enabled: true
server:
  shutdown: graceful
NeedUse
Per-environment valuesapplication-<profile>.yml + SPRING_PROFILES_ACTIVE
SecretsEnvironment variables (APP_PAYMENTS_API_KEY) or spring.config.import from a secret store; never commit them
Feature switch read in many placesA properties record, not scattered @Value
One-off value in a test@TestPropertySource or @DynamicPropertySource

Web layer

@RestController
@RequestMapping("/api/orders")
class OrderController {
  private final OrderService orders;
  OrderController(OrderService orders) { this.orders = orders; }

  @PostMapping
  ResponseEntity<OrderDto> create(@Valid @RequestBody CreateOrder req, UriComponentsBuilder uri) {
    OrderDto created = orders.create(req);
    return ResponseEntity.created(uri.path("/api/orders/{id}").build(created.id())).body(created);
  }

  @GetMapping("/{id}")
  OrderDto get(@PathVariable UUID id) { return orders.get(id); }
}
  • Constructor injection only; no field @Autowired. A single constructor needs no annotation.
  • Request and response types are records (DTOs), never JPA entities. Returning an entity leaks columns, triggers lazy loading during serialization and couples the API to the schema.
  • Map domain exceptions to ProblemDetail in one @RestControllerAdvice:
@RestControllerAdvice
class ApiErrors extends ResponseEntityExceptionHandler {
  @ExceptionHandler(OrderNotFound.class)
  ProblemDetail notFound(OrderNotFound ex) {
    ProblemDetail pd = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
    pd.setType(URI.create("https://errors.acme.dev/order-not-found"));
    return pd;
  }
}

Extending ResponseEntityExceptionHandler makes validation failures (MethodArgumentNotValidException) come back as problem details too.

Outbound HTTP: use RestClient (blocking, fine with virtual threads) built from the auto-configured RestClient.Builder, with explicit connect and read timeouts.

Data layer

  • Put @Transactional on service methods, readOnly = true for queries. Never on controllers. Self-invocation (this.save()) bypasses the proxy, so the annotation does nothing.
  • Keep spring.jpa.open-in-view=false. Fix the resulting LazyInitializationException by fetching what the use case needs: a DTO projection, @EntityGraph(attributePaths = "lines"), or join fetch in a @Query.
  • Schema changes go through Flyway (db/migration/V3__add_order_status.sql) and spring.jpa.hibernate.ddl-auto=validate. Never update outside a throwaway prototype.
  • Turn on spring.jpa.properties.hibernate.generate_statistics=true or log SQL in a test to count queries when you suspect N+1.
SymptomFix
N+1 selects on a list endpoint@EntityGraph or fetch-join query; projection for read-only lists
LazyInitializationExceptionFetch inside the transaction; never re-enable open-in-view to hide it
Optimistic lock failures@Version column plus retry at the service boundary, or a 409 to the client
Slow bulk inserthibernate.jdbc.batch_size, sequence IDs (IDENTITY disables batching)

Operations

  • Actuator: expose only what you need (management.endpoints.web.exposure.include=health,info,prometheus) and enable probes (management.endpoint.health.probes.enabled=true) for /actuator/health/liveness and /readiness. Keep the DB out of liveness.
  • Graceful shutdown is the default from Boot 3.4; set spring.lifecycle.timeout-per-shutdown-phase below the orchestrator’s termination grace period.
  • Virtual threads: avoid synchronized blocks around blocking I/O on Java 21-23 (pinning); Java 24+ removed most pinning. Size connection pools for the database, not for thread count.

Testing

LayerAnnotationBoots
Controller, JSON, validation@WebMvcTest(OrderController.class) + @MockitoBean OrderServiceMVC only
Repository queries@DataJpaTest + TestcontainersJPA + DB
Full flow@SpringBootTest(webEnvironment = RANDOM_PORT)Everything
@SpringBootTest
@Testcontainers
class OrderFlowTest {
  @Container @ServiceConnection
  static PostgreSQLContainer<?> db = new PostgreSQLContainer<>("postgres:17");
}

@ServiceConnection replaces hand-written @DynamicPropertySource URL plumbing. Testcontainers needs a Docker daemon; in a Codex sandbox without one, say so and run the slices that do not need it. Use @AutoConfigureTestDatabase(replace = NONE) with @DataJpaTest so it does not swap in H2 for the real container.

Context caching: every distinct combination of @MockitoBean, profiles and properties creates a new application context. Many unique combinations is the usual cause of a slow suite; consolidate them into shared base configurations.

Workflow checklist

  • Read AGENTS.md, build file, Boot version, application.yml
  • Run the existing tests to get a baseline
  • Add migration, entity, repository, service (transactional), DTOs, controller
  • Map new failure cases to ProblemDetail
  • Add a slice test per layer touched plus one integration test for the flow
  • ./mvnw verify or ./gradlew check green; start the app and hit the endpoint once

Gotchas

  • @Transactional on a private method or called from the same class does nothing.
  • @MockBean is deprecated since Boot 3.4; use @MockitoBean from spring-test.
  • Jackson serializing Instant as numbers: set spring.jackson.serialization.write-dates-as-timestamps=false.
  • @Async and @Scheduled need @EnableAsync / @EnableScheduling, and both lose the caller’s transaction and security context.
  • ddl-auto=create-drop left in a shared profile wipes a real database on restart. Ask before running anything against a non-local datasource.

Output

When reporting, list: endpoints added or changed (method, path, status codes), migrations added, properties introduced with defaults, tests added and the exact build command run with its result.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Spring Boot"
  short_description: "Typed config, thin controllers, fast test slices"
  default_prompt: "$spring-boot Add a POST /api/orders endpoint with validation and a Flyway migration"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc