Spring Boot Engineer
Core Workflow
- Setup check — run Setup Check below before writing any code.
- Design first — for non-trivial work, confirm service boundaries, data model, security needs, and reactive-vs-servlet choice before coding.
- Implement bottom-up — entity → repository → service → controller. Constructor injection only. Write DTOs as records (Java) or
data class (Kotlin), never expose JPA entities from the web layer.
- Secure —
@PreAuthorize / SecurityFilterChain, externalize secrets, validate all input with @Valid.
- Test — slice tests (
@WebMvcTest, @DataJpaTest) for fast feedback, one @SpringBootTest per critical flow, Testcontainers for anything that touches a real DB.
- Verify — run
./mvnw test or ./gradlew test and confirm /actuator/health returns UP before declaring done.
Setup Check
Mandatory before any code change.
Step 1 — Is this actually a Spring Boot project?
Look for one of these, in this order:
spring-boot-starter-parent or spring-boot-dependencies in pom.xml.
org.springframework.boot plugin in build.gradle / build.gradle.kts.
- A class annotated with
@SpringBootApplication.
If none → stop and tell the user this isn't a Spring Boot project before proceeding.
Step 2 — Kotlin project? Verify required compiler plugins.
If src/main/kotlin/ exists and Spring / JPA is used, both plugins below must be configured. Without them Spring proxies and JPA entities fail at runtime with cryptic errors:
| Plugin |
Why it's needed |
kotlin("plugin.spring") (kotlin-spring) |
Opens classes annotated with @Component / @Service / @Configuration / @Transactional / @Async / @Cacheable / @SpringBootTest — CGLIB proxies cannot subclass final classes. |
kotlin("plugin.jpa") (kotlin-jpa) |
Generates a no-arg constructor for @Entity / @Embeddable / @MappedSuperclass. Required for JPA to instantiate entities via reflection. |
If either is missing → suggest adding it before implementing anything that relies on it.
plugins {
kotlin("plugin.spring") version "<kotlin-version>"
kotlin("plugin.jpa") version "<kotlin-version>"
}
Also confirm kotlin-reflect is on the classpath (included by spring-boot-starter).
Step 3 — Java version & runtime
Spring Boot 4.x requires Java 17+ (Java 21+ recommended — first-class virtual thread support). Check <java.version> (Maven) or java.toolchain / sourceCompatibility (Gradle).
Virtual threads: enable via spring.threads.virtual.enabled=true. Don't use them unconditionally — harmful with synchronized blocks and ThreadLocal-heavy libraries.
Undertow is no longer supported in Boot 4 (dropped Servlet 6.1 compatibility). Use Tomcat (default) or Jetty.
Step 4 — Jackson version
Spring Boot 4 defaults to Jackson 3. Most packages were renamed: com.fasterxml.jackson → tools.jackson. Exception: jackson-annotations intentionally keeps the old namespace (com.fasterxml.jackson.annotation) for backward compatibility. Any code importing jackson-databind or jackson-core classes directly will break — update imports. No official compatibility bridge exists; migration must be done manually. If the project already uses Jackson 3 → proceed. If still on Jackson 2 → flag the migration before adding new Jackson-dependent code.
Reference Guide
Load on demand — don't read all of these upfront.
| Topic |
Reference |
Load when |
| Web Layer |
references/web.md |
Controllers, DTO boundary, validation, ProblemDetail, pagination, CORS, deprecations |
| Data Access |
references/data.md |
JPA / Hibernate pitfalls: N+1, open-in-view, @Transactional self-invocation, fetch-join + pagination, Hikari tuning |
| Security |
references/security.md |
Spring Security 7 SecurityFilterChain, CSRF rules, JWT resource server, method security, /actuator/* hardening |
| Testing |
references/testing.md |
Test slices (@WebMvcTest / @DataJpaTest / @RestClientTest), @MockitoBean migration, Testcontainers + @ServiceConnection |
| Migrations |
references/migrations.md |
Flyway / Liquibase, zero-downtime schema change (expand-contract), baseline-on-migrate, CREATE INDEX CONCURRENTLY |
| Scheduling & Observability |
references/scheduling-observability.md |
@Scheduled in a cluster (ShedLock), Actuator exposure, health probes, Micrometer cardinality, virtual threads trade-offs |
| Kotlin |
references/kotlin.md |
kotlin-spring / kotlin-jpa plugins, @field: validation, suspend controllers, data class vs @Entity |
| Event-Driven |
references/event-driven.md |
@TransactionalEventListener, @Async traps, Kafka idempotence, outbox pattern |
| Resilience |
references/resilience.md |
Resilience4j annotation order, fallbackMethod rules, breaker + retry interaction, distributed vs local rate-limit |
| Reactive (WebFlux) |
references/reactive.md |
When WebFlux is the right choice, .block() traps, Schedulers.boundedElastic(), context propagation, backpressure |
| Cloud Native |
references/cloud.md |
spring.config.import (not bootstrap.yml), @RefreshScope limits, Gateway on WebFlux, k8s vs Spring Cloud choice |
Constraints
MUST DO
| Rule |
Correct pattern |
| Constructor injection |
public MyService(Dep dep) { this.dep = dep; } — never @Autowired on a field |
| Validate every mutating endpoint |
@Valid @RequestBody MyRequest req + Bean Validation annotations on the DTO |
| DTOs at the web boundary |
Java record or Kotlin data class — never return or accept JPA entities directly |
| Type-safe config |
@ConfigurationProperties(prefix = "app") bound to a record/class, not @Value("${…}") scattered across the codebase |
| Correct stereotype |
@Service for business logic, @Repository for data, @RestController for HTTP, @Component only when nothing else fits |
| Transaction scope |
@Transactional only on public methods of a Spring-managed bean, called from outside the class (see MUST NOT below) |
| Read-only hint |
@Transactional(readOnly = true) on queries — lets Hibernate skip dirty-checking |
| Rollback on checked exceptions |
@Transactional(rollbackFor = Exception.class) when the method throws checked exceptions you want to roll back |
| Global error handling |
@RestControllerAdvice + ProblemDetail (RFC 7807) — never leak stack traces to clients |
| Externalize secrets |
Env vars or Spring Cloud Config — never commit secrets to application.properties / application.yml |
| Kotlin + Spring |
kotlin("plugin.spring") always; kotlin("plugin.jpa") when JPA is used |
| Kotlin validation |
@field:NotBlank on data class properties — bare @NotBlank is silently ignored |
| Post-commit side effects |
@TransactionalEventListener(phase = AFTER_COMMIT) for email / Kafka / external calls — never inline after a save() inside the same transaction |
MUST NOT — @Transactional pitfalls that break in production
- Self-invocation. Calling
this.methodWithTransactional() from another method in the same bean bypasses the proxy — no transaction starts. If you need it, inject self (@Lazy @Autowired MyService self) or extract the method to a separate bean.
- Private / package-private /
final methods. Proxies cannot intercept them. @Transactional must be on public non-final methods. (In Kotlin: add kotlin-spring plugin so classes/methods are open.)
- Checked exceptions without
rollbackFor. By default Spring rolls back only on RuntimeException / Error. Declare @Transactional(rollbackFor = IOException.class) (or a common superclass) when you want checked exceptions to roll back.
@Async + @Transactional on the same method. The async thread doesn't inherit the transaction context — entity becomes detached, you get LazyInitializationException or no transaction at all. Split into two beans or use @TransactionalEventListener(phase = AFTER_COMMIT).
- Writes inside
readOnly = true. Hibernate may skip the flush — your update silently disappears.
@Transactional on a @PostConstruct method. Proxy isn't fully initialized yet; the annotation has no effect.
MUST NOT — general
- Field injection (
@Autowired on fields) — breaks testability and hides required dependencies.
- Skipping
@Valid on API input — request bodies reach your service with whatever the client sent.
- Using
@Component when a more specific stereotype fits.
- Mixing blocking and reactive code: no
.block() / .toFuture().get() inside a Mono / Flux chain; no blocking JDBC inside a WebFlux controller. Wrap unavoidable blocking calls with Mono.fromCallable(...).subscribeOn(Schedulers.boundedElastic()).
- Storing secrets, connection strings or tokens in
application.properties / application.yml committed to git.
- Hardcoding URLs / environment-specific values — use profiles (
application-dev.yml, application-prod.yml) and env vars.
- Removed Boot 3.x deprecated APIs — all of these are gone in Boot 4 and will fail to compile:
WebSecurityConfigurerAdapter (use SecurityFilterChain bean)
antMatchers(...) (use requestMatchers(...))
WebMvcConfigurerAdapter (implement WebMvcConfigurer)
and() in HttpSecurity DSL (use separate lambda calls)
@MockBean / @SpyBean (use @MockitoBean / @MockitoSpyBean)
authorizeRequests() (use authorizeHttpRequests())
- Jackson 3 imports:
com.fasterxml.jackson.* → tools.jackson.*. Don't write new code against Jackson 2 packages on a Boot 4 project.
- Undertow embedded server — not supported. Don't add
spring-boot-starter-undertow.
- Returning or accepting JPA entities at the controller layer — leaks persistence details, causes lazy-loading blowups (
could not initialize proxy — no Session), breaks API contracts on entity refactors.
- N+1 queries:
repository.findAll() followed by accessing @OneToMany lazy associations in a loop. Use @EntityGraph, JOIN FETCH, or projections. See references/data.md.
spring.jpa.open-in-view=true in production (the Spring Boot default!). Explicitly set it to false — OSIV hides lazy-loading bugs and holds the DB connection for the entire HTTP request.
Output Format
When implementing a new feature, deliver in this order:
- Migration (Flyway / Liquibase) if schema changes are needed.
- Entity + Repository.
- Service with
@Transactional boundaries.
- DTOs (request + response) as records / data classes.
- Controller +
@RestControllerAdvice entries for new exception types.
- Tests: one
@DataJpaTest for repository custom queries, one @WebMvcTest per controller, one @SpringBootTest for the full happy path.
- One-line summary of the key architectural decisions (why this transaction boundary, why this projection, why this status code).
1---2name: spring-boot-engineer3description: Use when building, modifying, or reviewing Spring Boot 4.x applications (Spring Framework 7, Spring Security 7, Hibernate 7, Jackson 3) in Java or Kotlin — REST controllers, Spring Data JPA repositories, OAuth2 / JWT, WebFlux reactive endpoints, Kafka / event-driven code, Resilience4j, Spring Cloud, or Spring Boot tests. Covers common pitfalls that break in production: @Transactional self-invocation, N+1 queries, Kotlin + JPA plugins, blocking calls inside WebFlux. Migrating from Boot 3.x? See Setup Check — major breaking changes: Jackson 3 package rename, @MockBean removed, and() in Security DSL removed, Undertow dropped.4---56# Spring Boot Engineer78## Core Workflow9101. **Setup check** — run Setup Check below before writing any code.112. **Design first** — for non-trivial work, confirm service boundaries, data model, security needs, and reactive-vs-servlet choice before coding.123. **Implement bottom-up** — entity → repository → service → controller. Constructor injection only. Write DTOs as records (Java) or `data class` (Kotlin), never expose JPA entities from the web layer.134. **Secure** — `@PreAuthorize` / `SecurityFilterChain`, externalize secrets, validate all input with `@Valid`.145. **Test** — slice tests (`@WebMvcTest`, `@DataJpaTest`) for fast feedback, one `@SpringBootTest` per critical flow, Testcontainers for anything that touches a real DB.156. **Verify** — run `./mvnw test` or `./gradlew test` and confirm `/actuator/health` returns `UP` before declaring done.1617## Setup Check1819**Mandatory before any code change.**2021### Step 1 — Is this actually a Spring Boot project?2223Look for one of these, in this order:2425- `spring-boot-starter-parent` or `spring-boot-dependencies` in `pom.xml`.26- `org.springframework.boot` plugin in `build.gradle` / `build.gradle.kts`.27- A class annotated with `@SpringBootApplication`.2829If none → stop and tell the user this isn't a Spring Boot project before proceeding.3031### Step 2 — Kotlin project? Verify required compiler plugins.3233If `src/main/kotlin/` exists **and** Spring / JPA is used, both plugins below must be configured. Without them Spring proxies and JPA entities fail at runtime with cryptic errors:3435| Plugin | Why it's needed |36|---|---|37| `kotlin("plugin.spring")` (`kotlin-spring`) | Opens classes annotated with `@Component` / `@Service` / `@Configuration` / `@Transactional` / `@Async` / `@Cacheable` / `@SpringBootTest` — CGLIB proxies cannot subclass `final` classes. |38| `kotlin("plugin.jpa")` (`kotlin-jpa`) | Generates a no-arg constructor for `@Entity` / `@Embeddable` / `@MappedSuperclass`. Required for JPA to instantiate entities via reflection. |3940If either is missing → **suggest adding it** before implementing anything that relies on it.4142```kotlin43plugins {44 kotlin("plugin.spring") version "<kotlin-version>"45 kotlin("plugin.jpa") version "<kotlin-version>"46}47```4849Also confirm `kotlin-reflect` is on the classpath (included by `spring-boot-starter`).5051### Step 3 — Java version & runtime5253Spring Boot 4.x requires **Java 17+** (Java 21+ recommended — first-class virtual thread support). Check `<java.version>` (Maven) or `java.toolchain` / `sourceCompatibility` (Gradle).5455Virtual threads: enable via `spring.threads.virtual.enabled=true`. Don't use them unconditionally — harmful with `synchronized` blocks and `ThreadLocal`-heavy libraries.5657**Undertow is no longer supported** in Boot 4 (dropped Servlet 6.1 compatibility). Use Tomcat (default) or Jetty.5859### Step 4 — Jackson version6061Spring Boot 4 defaults to **Jackson 3**. Most packages were renamed: `com.fasterxml.jackson` → `tools.jackson`. Exception: `jackson-annotations` intentionally keeps the old namespace (`com.fasterxml.jackson.annotation`) for backward compatibility. Any code importing `jackson-databind` or `jackson-core` classes directly will break — update imports. No official compatibility bridge exists; migration must be done manually. If the project already uses Jackson 3 → proceed. If still on Jackson 2 → flag the migration before adding new Jackson-dependent code.6263## Reference Guide6465Load on demand — don't read all of these upfront.6667| Topic | Reference | Load when |68|-------|-----------|-----------|69| Web Layer | `references/web.md` | Controllers, DTO boundary, validation, `ProblemDetail`, pagination, CORS, deprecations |70| Data Access | `references/data.md` | JPA / Hibernate pitfalls: N+1, `open-in-view`, `@Transactional` self-invocation, fetch-join + pagination, Hikari tuning |71| Security | `references/security.md` | Spring Security 7 `SecurityFilterChain`, CSRF rules, JWT resource server, method security, `/actuator/*` hardening |72| Testing | `references/testing.md` | Test slices (`@WebMvcTest` / `@DataJpaTest` / `@RestClientTest`), `@MockitoBean` migration, Testcontainers + `@ServiceConnection` |73| Migrations | `references/migrations.md` | Flyway / Liquibase, zero-downtime schema change (expand-contract), baseline-on-migrate, `CREATE INDEX CONCURRENTLY` |74| Scheduling & Observability | `references/scheduling-observability.md` | `@Scheduled` in a cluster (ShedLock), Actuator exposure, health probes, Micrometer cardinality, virtual threads trade-offs |75| Kotlin | `references/kotlin.md` | `kotlin-spring` / `kotlin-jpa` plugins, `@field:` validation, `suspend` controllers, `data class` vs `@Entity` |76| Event-Driven | `references/event-driven.md` | `@TransactionalEventListener`, `@Async` traps, Kafka idempotence, outbox pattern |77| Resilience | `references/resilience.md` | Resilience4j annotation order, `fallbackMethod` rules, breaker + retry interaction, distributed vs local rate-limit |78| Reactive (WebFlux) | `references/reactive.md` | When WebFlux is the right choice, `.block()` traps, `Schedulers.boundedElastic()`, context propagation, backpressure |79| Cloud Native | `references/cloud.md` | `spring.config.import` (not `bootstrap.yml`), `@RefreshScope` limits, Gateway on WebFlux, k8s vs Spring Cloud choice |8081## Constraints8283### MUST DO8485| Rule | Correct pattern |86|------|-----------------|87| Constructor injection | `public MyService(Dep dep) { this.dep = dep; }` — never `@Autowired` on a field |88| Validate every mutating endpoint | `@Valid @RequestBody MyRequest req` + Bean Validation annotations on the DTO |89| DTOs at the web boundary | Java `record` or Kotlin `data class` — **never** return or accept JPA entities directly |90| Type-safe config | `@ConfigurationProperties(prefix = "app")` bound to a record/class, not `@Value("${…}")` scattered across the codebase |91| Correct stereotype | `@Service` for business logic, `@Repository` for data, `@RestController` for HTTP, `@Component` only when nothing else fits |92| Transaction scope | `@Transactional` only on `public` methods of a Spring-managed bean, called from outside the class (see MUST NOT below) |93| Read-only hint | `@Transactional(readOnly = true)` on queries — lets Hibernate skip dirty-checking |94| Rollback on checked exceptions | `@Transactional(rollbackFor = Exception.class)` when the method throws checked exceptions you want to roll back |95| Global error handling | `@RestControllerAdvice` + `ProblemDetail` (RFC 7807) — never leak stack traces to clients |96| Externalize secrets | Env vars or Spring Cloud Config — never commit secrets to `application.properties` / `application.yml` |97| Kotlin + Spring | `kotlin("plugin.spring")` always; `kotlin("plugin.jpa")` when JPA is used |98| Kotlin validation | `@field:NotBlank` on `data class` properties — bare `@NotBlank` is silently ignored |99| Post-commit side effects | `@TransactionalEventListener(phase = AFTER_COMMIT)` for email / Kafka / external calls — never inline after a `save()` inside the same transaction |100101### MUST NOT — `@Transactional` pitfalls that break in production102103- **Self-invocation.** Calling `this.methodWithTransactional()` from another method in the same bean bypasses the proxy — no transaction starts. If you need it, inject `self` (`@Lazy @Autowired MyService self`) or extract the method to a separate bean.104- **Private / package-private / `final` methods.** Proxies cannot intercept them. `@Transactional` must be on `public` non-`final` methods. (In Kotlin: add `kotlin-spring` plugin so classes/methods are open.)105- **Checked exceptions without `rollbackFor`.** By default Spring rolls back only on `RuntimeException` / `Error`. Declare `@Transactional(rollbackFor = IOException.class)` (or a common superclass) when you want checked exceptions to roll back.106- **`@Async` + `@Transactional` on the same method.** The async thread doesn't inherit the transaction context — entity becomes detached, you get `LazyInitializationException` or no transaction at all. Split into two beans or use `@TransactionalEventListener(phase = AFTER_COMMIT)`.107- **Writes inside `readOnly = true`.** Hibernate may skip the flush — your update silently disappears.108- **`@Transactional` on a `@PostConstruct` method.** Proxy isn't fully initialized yet; the annotation has no effect.109110### MUST NOT — general111112- Field injection (`@Autowired` on fields) — breaks testability and hides required dependencies.113- Skipping `@Valid` on API input — request bodies reach your service with whatever the client sent.114- Using `@Component` when a more specific stereotype fits.115- Mixing blocking and reactive code: no `.block()` / `.toFuture().get()` inside a `Mono` / `Flux` chain; no blocking JDBC inside a WebFlux controller. Wrap unavoidable blocking calls with `Mono.fromCallable(...).subscribeOn(Schedulers.boundedElastic())`.116- Storing secrets, connection strings or tokens in `application.properties` / `application.yml` committed to git.117- Hardcoding URLs / environment-specific values — use profiles (`application-dev.yml`, `application-prod.yml`) and env vars.118- Removed Boot 3.x deprecated APIs — all of these are gone in Boot 4 and will fail to compile:119 - `WebSecurityConfigurerAdapter` (use `SecurityFilterChain` bean)120 - `antMatchers(...)` (use `requestMatchers(...)`)121 - `WebMvcConfigurerAdapter` (implement `WebMvcConfigurer`)122 - `and()` in `HttpSecurity` DSL (use separate lambda calls)123 - `@MockBean` / `@SpyBean` (use `@MockitoBean` / `@MockitoSpyBean`)124 - `authorizeRequests()` (use `authorizeHttpRequests()`)125- **Jackson 3 imports**: `com.fasterxml.jackson.*` → `tools.jackson.*`. Don't write new code against Jackson 2 packages on a Boot 4 project.126- Undertow embedded server — not supported. Don't add `spring-boot-starter-undertow`.127- Returning or accepting JPA entities at the controller layer — leaks persistence details, causes lazy-loading blowups (`could not initialize proxy — no Session`), breaks API contracts on entity refactors.128- N+1 queries: `repository.findAll()` followed by accessing `@OneToMany` lazy associations in a loop. Use `@EntityGraph`, `JOIN FETCH`, or projections. See `references/data.md`.129- `spring.jpa.open-in-view=true` in production (the Spring Boot default!). Explicitly set it to `false` — OSIV hides lazy-loading bugs and holds the DB connection for the entire HTTP request.130131## Output Format132133When implementing a new feature, deliver in this order:1341. Migration (Flyway / Liquibase) if schema changes are needed.1352. Entity + Repository.1363. Service with `@Transactional` boundaries.1374. DTOs (request + response) as records / data classes.1385. Controller + `@RestControllerAdvice` entries for new exception types.1396. Tests: one `@DataJpaTest` for repository custom queries, one `@WebMvcTest` per controller, one `@SpringBootTest` for the full happy path.1407. One-line summary of the key architectural decisions (why this transaction boundary, why this projection, why this status code).