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.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- spring-boot Installs to ~/.agents/skills/spring-boot/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install spring-boot The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project spring-boot Installs to .agents/skills/spring-boot/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/spring-boot.zipunzip -o spring-boot.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) spring-boot PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
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.
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
- Read
AGENTS.md, then the build file. Note the build tool (./mvnwor./gradlew; always use the wrapper), the Boot version, the Java toolchain, and which starters exist. - Check the Boot line: 3.2+ has
RestClientand virtual threads, 3.4+ has@MockitoBeanandMockMvcTester. Boot 4 (Spring Framework 7, Jackson 3) changes package names and starters; do not mix Boot 4 advice into a 3.x project. - Read
src/main/resources/application.yml(or.properties) and any profile files. - Run the tests once before editing:
./mvnw -q testor./gradlew test. Dependency resolution needs network; in an offline sandbox, use what is in~/.m2or~/.gradleand 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
| Need | Use |
|---|---|
| Per-environment values | application-<profile>.yml + SPRING_PROFILES_ACTIVE |
| Secrets | Environment variables (APP_PAYMENTS_API_KEY) or spring.config.import from a secret store; never commit them |
| Feature switch read in many places | A 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
ProblemDetailin 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
@Transactionalon service methods,readOnly = truefor 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 resultingLazyInitializationExceptionby fetching what the use case needs: a DTO projection,@EntityGraph(attributePaths = "lines"), orjoin fetchin a@Query. - Schema changes go through Flyway (
db/migration/V3__add_order_status.sql) andspring.jpa.hibernate.ddl-auto=validate. Neverupdateoutside a throwaway prototype. - Turn on
spring.jpa.properties.hibernate.generate_statistics=trueor log SQL in a test to count queries when you suspect N+1.
| Symptom | Fix |
|---|---|
| N+1 selects on a list endpoint | @EntityGraph or fetch-join query; projection for read-only lists |
LazyInitializationException | Fetch 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 insert | hibernate.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/livenessand/readiness. Keep the DB out of liveness. - Graceful shutdown is the default from Boot 3.4; set
spring.lifecycle.timeout-per-shutdown-phasebelow the orchestrator’s termination grace period. - Virtual threads: avoid
synchronizedblocks 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
| Layer | Annotation | Boots |
|---|---|---|
| Controller, JSON, validation | @WebMvcTest(OrderController.class) + @MockitoBean OrderService | MVC only |
| Repository queries | @DataJpaTest + Testcontainers | JPA + 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 verifyor./gradlew checkgreen; start the app and hit the endpoint once
Gotchas
@Transactionalon aprivatemethod or called from the same class does nothing.@MockBeanis deprecated since Boot 3.4; use@MockitoBeanfromspring-test.- Jackson serializing
Instantas numbers: setspring.jackson.serialization.write-dates-as-timestamps=false. @Asyncand@Scheduledneed@EnableAsync/@EnableScheduling, and both lose the caller’s transaction and security context.ddl-auto=create-dropleft 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.yaml
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.