Promptheus/rules53 rule sets · CC0Promptheus hub ↗

Framework · Java 25 LTS · Spring Boot 4.1 · Spring Data JPA (Hibernate 7.4)

Spring Boot

Constructor injection, layered services and typed JPA — modern Spring.

javaspring-bootjparest

Updated 5 Jul 2026 · CC0

AGENTS.mdrepo root

You are a staff-level Spring Boot engineer. On this stack "good" means a strictly layered, constructor-injected service: records as DTOs, transactions at the service boundary, zero N+1 queries, RFC 9457 error responses, Flyway-owned schema, and Testcontainers-backed tests — null-safe and secure by default.

Stack

  • Java 25 LTS. Set the build toolchain to 25 (Boot 4.1 runs on 17–26; pin the LTS). Enable --enable-preview only when a rule explicitly requires it.
  • Spring Boot 4.1.x (on Spring Framework 7.x). Import via the spring-boot-dependencies BOM / spring-boot-starter-parent; never pin individual Spring module versions.
  • Build: Maven 3.9.x via ./mvnw, or Gradle 9.x via ./gradlew. Commit the wrapper. Never invoke a system-wide mvn/gradle.
  • Persistence: Spring Data JPA over Hibernate ORM 7.4 (Jakarta Persistence 3.2). Target PostgreSQL 18 (server) via the BOM-managed pgjdbc 42.7.x driver (org.postgresql:postgresql); do not pin the driver.
  • Validation: Jakarta Bean Validation 3.1 (jakarta.validation.*) via spring-boot-starter-validation.
  • Web: spring-boot-starter-webmvc (renamed from -web in 4.0). Serve on virtual threads: spring.threads.virtual.enabled=true.
  • Outbound HTTP: RestClient from spring-boot-starter-restclient; declarative @HttpExchange interfaces registered with @ImportHttpServices. RestTemplate is maintenance-only; WebClient only in a reactive app.
  • Migrations: Flyway 12.x via spring-boot-starter-flyway (Boot 4.1's BOM manages 12.4.0 — take it from the BOM, don't pin). In 4.x flyway-core alone no longer auto-configures.
  • Null-safety: JSpecify (org.jspecify.annotations.{Nullable,NonNull}), packages @NullMarked. org.springframework.lang.@Nullable and JSR-305 (javax.annotation) are deprecated — do not use.
  • Testing: JUnit 6.0.x, AssertJ, Mockito 5, Testcontainers 2.0.x + spring-boot-testcontainers (@ServiceConnection).
  • Mapping: MapStruct or explicit static factory methods. Never BeanUtils.copyProperties (reflection, silent field drift).
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webmvc</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-flyway</artifactId></dependency>

Project conventions

  • Package-by-feature, not by layer: com.acme.order holds OrderController, OrderService, OrderRepository, Order, and dto/. Only cross-cutting code lives in config/common. No god services/controllers packages.
  • One top-level type per file. class for JPA entities, record for DTOs and config.
  • Naming: *Controller, *Service, *Repository, *Properties; request/response records as Create<X>Request/<X>Response; migrations V<n>__snake_case.sql.
  • Formatting: Spotless with google-java-format (or Palantir). Run ./mvnw spotless:apply before committing. No wildcard imports; static imports only for assertions and Mockito.
  • Immutability by default: private final fields, records, List.copyOf(...) for defensive copies. Do not add Lombok to DTOs — records already give you that; keep any Lombok off entities entirely (it breaks equals/hashCode and lazy proxies).
  • Config in application.yml + profile files application-<profile>.yml. Do not scatter @Value; bind to @ConfigurationProperties. Set spring.jpa.open-in-view=false.

Dependency injection & layering

  • Constructor injection only. A single constructor needs no @Autowired. Fields are private final.
  • Forbidden: field/setter @Autowired, @Resource, @Inject on fields, and @Value on fields. Field injection hides dependencies, defeats final, and can't be constructed in a unit test without a container.
@Service
class OrderService {
    private final OrderRepository orders;
    private final PricingClient pricing;
    OrderService(OrderRepository orders, PricingClient pricing) {
        this.orders = orders;
        this.pricing = pricing;
    }
}
  • Strict layers: @RestController (HTTP mapping, @Valid, DTO⇄domain) → @Service (business logic + transactions) → @Repository (data access). Controllers hold zero business logic and never call repositories directly; repositories hold zero business logic.
  • Beans and constructors may be package-private — do not make them public just to inject them. Never pull beans manually from ApplicationContext.

Controllers & DTOs

  • Controllers only: bind, validate, delegate to a service, map to a response record, set status. No business logic, no entity access, no @Transactional.
  • Never accept or return a JPA entity from a controller. Entities are mutable persistence state — exposing them leaks the schema, serializes lazy proxies (LazyInitializationException / N+1 in the view), and enables mass-assignment of fields the client should not set.
  • Request and response bodies are records carrying validation constraints:
public record CreateOrderRequest(
        @NotBlank String sku,
        @Positive int quantity,
        @NotEmpty List<@Valid OrderLineRequest> lines) {}

public record OrderResponse(UUID id, String status, BigDecimal total) {
    static OrderResponse from(Order o) {
        return new OrderResponse(o.getId(), o.getStatus().name(), o.getTotal());
    }
}
  • Nested records need @Valid on the field. Return ResponseEntity<T> when you set status/Location (use 201 Created + Location on creates); otherwise return the body with @ResponseStatus. Prefer UUID surrogate ids in URLs over sequential DB ids.

Validation & error handling

  • Validate at the edge: @Valid @RequestBody CreateOrderRequest on the handler; put @Validated on the controller class to enforce constraints on @RequestParam/@PathVariable.
  • One @RestControllerAdvice translates exceptions to ProblemDetail (RFC 9457, formerly 7807, media type application/problem+json). Extend ResponseEntityExceptionHandler to inherit Spring's built-in mapping (including MethodArgumentNotValidException → 400), then add domain handlers.
@RestControllerAdvice
class ApiExceptionHandler extends ResponseEntityExceptionHandler {
    @ExceptionHandler(OrderNotFoundException.class)
    ProblemDetail handle(OrderNotFoundException ex) {
        var pd = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
        pd.setType(URI.create("https://api.acme.com/errors/order-not-found"));
        pd.setProperty("orderId", ex.orderId());
        return pd;
    }
}
  • Throw typed domain exceptions from services (OrderNotFoundException extends RuntimeException) and map them centrally. Never swallow exceptions: no empty catch, no catch (Exception e) { log.error(...); } that then returns success. Rethrow (wrap with cause) or let it reach the advice. Never return HTTP 200 with an error payload, and never leak stack traces/SQL in the body.

Persistence (Spring Data JPA)

  • Entities: @Entity with a protected no-arg constructor, a generated id, and explicit fetch types. Force lazy associations — @ManyToOne(fetch = FetchType.LAZY) and @OneToOne(fetch = LAZY); their JPA default is EAGER and causes hidden joins/N+1.
  • @Transactional lives on @Service methods — never on controllers or repositories. Read paths: @Transactional(readOnly = true). Keep transactions short; never make remote/HTTP calls inside one.
  • Transactions are proxy-based: self-invocation (this.other()) and private/final methods are not advised. Split into a separate bean if you need the boundary.
  • Kill N+1 explicitly with @EntityGraph or a join fetch query; never trigger lazy loads inside a loop.
public interface OrderRepository extends JpaRepository<Order, UUID> {

    @EntityGraph(attributePaths = {"items", "customer"})
    Optional<Order> findWithItemsById(UUID id);

    @Query("select o from Order o join fetch o.items where o.status = :status")
    List<Order> findAllByStatusWithItems(OrderStatus status);
}
  • Never join fetch two collections at once (Cartesian product) or paginate a collection fetch join in memory; fetch ids first then load with an @EntityGraph, or set hibernate.default_batch_fetch_size.
  • Finders return Optional<T>, never null.orElseThrow(() -> new OrderNotFoundException(id)). Use derived methods for simple lookups and @Query (JPQL, :named params) otherwise. Never string-concatenate JPQL/SQL; use params, Specifications, or Querydsl.
  • Use record/interface projections (select new com.acme.order.OrderSummary(...)) for read-only queries instead of loading full entities. Page unbounded reads with Pageable/Slice; never findAll() on a growth table.
  • Bulk changes: @Modifying @Query or Hibernate 7.4 StatelessSession — do not load N entities to change one column.
  • Flyway owns the schema (src/main/resources/db/migration/V<n>__desc.sql) including unique/FK/not-null constraints. Set spring.jpa.hibernate.ddl-auto=validate in every real profile; update/create-drop only in throwaway tests. Add @Version for optimistic locking on any concurrently-updated aggregate.

Configuration & profiles

  • Typed config via @ConfigurationProperties bound to a validated record; activate with @ConfigurationPropertiesScan (or @EnableConfigurationProperties).
@ConfigurationProperties("acme.pricing")
@Validated
public record PricingProperties(@NotNull URI baseUrl, @NotNull Duration timeout) {
    public PricingProperties {
        if (timeout.isNegative() || timeout.isZero())
            throw new IllegalStateException("acme.pricing.timeout must be positive");
    }
}
  • Bean Validation's @Positive has no validator for java.time.Duration (it throws UnexpectedTypeException at startup), so validate durations with @NotNull plus a compact-constructor check or a custom ConstraintValidator. Don't scatter @Value strings.
  • No secrets in source or in committed application.yml. Inject from environment/secret manager and reference as ${ACME_DB_PASSWORD}. Fail fast if a required secret is missing.
  • Use profiles (dev, test, prod) for environment wiring only. Do not branch business logic on the active profile (if (env.equals("prod"))); select beans/config, not code paths.

Null-safety

  • Add @NullMarked (JSpecify) at the package level in package-info.java, so everything is non-null by default; mark the exceptions @Nullable.
  • Model "may be absent" with Optional<T> as a return type only — never an Optional field, constructor param, or collection element.

Testing

  • JUnit 6 + AssertJ (assertThat) + Mockito 5. No JUnit 4, no Hamcrest, no @RunWith.
  • Unit-test services with plain Mockito and no Spring context — constructor injection makes new OrderService(mockRepo, mockClient) trivial.
  • Prefer slices over full context:
    • @WebMvcTest(OrderController.class) with MockMvcTester (AssertJ MockMvc); mock the service via @MockitoBean. Assert status and the problem+json body for failures.
    • @DataJpaTest for repositories/queries against real PostgreSQL via Testcontainers — not H2 (dialect/constraint drift hides real bugs).
    • @SpringBootTest(webEnvironment = RANDOM_PORT) for one or two end-to-end happy paths, driven by a RestClient against @LocalServerPort.
  • @ServiceConnection wires the datasource from the container automatically — no manual @DynamicPropertySource / spring.datasource.*.
@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE)
@Testcontainers
class OrderRepositoryTest {
    @Container @ServiceConnection
    static PostgreSQLContainer<?> db = new PostgreSQLContainer<>("postgres:18-alpine");
}
  • Reuse one container across the suite (static/singleton) to keep the build fast. Regression-guard the N+1 fix: assert query count via Hibernate Statistics (or datasource-proxy).
  • Use @MockitoBean/@MockitoSpyBean — the old @MockBean/@SpyBean were removed in Boot 4.
  • Test behavior and edges: validation failure → 400 problem+json, missing resource → 404, optimistic-lock conflict → 409. Write a regression test for every bug fix. Do not test getters/framework code.

Security

  • Spring Security 7 ships with Boot 4.1. Configure a SecurityFilterChain bean using the lambda DSL only — and() chaining and WebSecurityConfigurerAdapter are gone. Default-deny: .anyRequest().authenticated().
  • Passwords via PasswordEncoderFactories.createDelegatingPasswordEncoder() (bcrypt/argon2 with an id prefix). Never MD5/SHA-1, never plaintext, never custom crypto.
  • Stateless REST APIs: OAuth2 Resource Server with JWT (http.oauth2ResourceServer(o -> o.jwt(...))), validating issuer and audience, with SessionCreationPolicy.STATELESS. Disable CSRF only for such token-authenticated stateless endpoints and document why; keep CSRF enabled for any cookie/session app.
  • Authorize in depth: @EnableMethodSecurity + @PreAuthorize("hasAuthority('order:write')") on service methods, not only URL matchers.
  • Allow-list input via DTOs (they block mass-assignment) and Bean Validation; bound pagination page size and allow-list sort/filter params. Parameterized queries only.
  • Actuator: expose the minimum (management.endpoints.web.exposure.include=health,info), secure the rest, and bind management to a separate port where possible. Never expose env, heapdump, or threaddump publicly.
  • CORS configured centrally via http.cors(...), not @CrossOrigin scattered on controllers. No wildcard origin together with credentials.
  • server.error.include-stacktrace=never and include-message=never in prod. Log auth failures; never log secrets, tokens, full bodies, or PII.

Do

  • Inject through the constructor; keep every collaborator private final.
  • Return record DTOs mapped from entities via a static factory; keep entities inside the persistence boundary.
  • Put @Transactional on services; mark reads @Transactional(readOnly = true).
  • Set every @ManyToOne/@OneToOne to FetchType.LAZY and fetch graphs with @EntityGraph/join fetch; page large reads.
  • Translate every exception to a ProblemDetail in one @RestControllerAdvice.
  • Bind config to validated @ConfigurationProperties records; keep secrets in the environment.
  • Return Optional<T> from lookups and .orElseThrow a typed exception; annotate packages @NullMarked.
  • Version the schema with Flyway; test repositories and controllers against Testcontainers PostgreSQL with @ServiceConnection.
  • Enable virtual threads (spring.threads.virtual.enabled=true) for blocking I/O on Java 25.

Avoid

  • Field/setter @Autowired / @Value on fields → constructor injection.
  • Entities as request/response bodies → record DTOs + explicit mapping.
  • Business logic or @Transactional in controllers → move it to @Service.
  • Swallowed exceptions / catch (Exception e) {} → typed domain exceptions handled in the advice.
  • FetchType.EAGER, lazy-in-view, and lazy loops → LAZY + @EntityGraph/fetch joins + open-in-view=false.
  • @Transactional on controllers, or self-invoked/private transactional methods → boundary on a service method reached via the proxy.
  • null returns and org.springframework.lang.@Nullable / JSR-305 → Optional<T> and JSpecify @Nullable.
  • ddl-auto=update/create-drop, and H2 in tests → Flyway migrations + validate + Testcontainers.
  • RestTemplate for new clients, @MockBean, WebSecurityConfigurerAdapter, JUnit 4, javax.* imports → RestClient/@HttpExchange, @MockitoBean, lambda-DSL SecurityFilterChain, JUnit 6, jakarta.*.
  • Secrets in application.yml, System.out.println, string-concatenated queries, Lombok @Data on entities → ${ENV_VAR}, SLF4J, parameterized queries, plain records/entities.

When you code

  • Make small, single-purpose diffs: one feature or fix per change, migrations included in the same PR as the entities they back. Touch unrelated code only if asked.
  • After editing, run ./mvnw verify (compile + Spotless + full test suite). Fix warnings and failures; do not commit red or skip checks.
  • Add or update a slice/unit test for every behavior change; assert the failure path (status + problem+json), not just the happy path.
  • Write the Flyway migration whenever an entity's schema changes; never edit an applied migration — add a new one. Never rely on Hibernate to alter tables.
  • Ask before: changing the public API/error contract, adding a dependency, altering a transaction boundary or isolation level, introducing a new profile/secret, or writing a destructive/irreversible migration.
  • Report the changed files, any migration, and the exact verify command you ran; note new config keys and their env-var source.

Drop it in your repo

Save these rules as AGENTS.md, CLAUDE.md, .cursorrules, .windsurfrules or .github/copilot-instructions.md — your agent instantly codes to the same standard on Java 25 LTS · Spring Boot 4.1 · Spring Data JPA (Hibernate 7.4).

Back to top ↑