agentskills.codes
SP

spring-boot-4-conventions

Spring Framework 7 / Spring Boot 4 idioms and defaults. Use when writing or reviewing controllers, services, configuration, HTTP clients, async/virtual-thread code, or anything touching Spring's programming model.

Install

mkdir -p .claude/skills/spring-boot-4-conventions && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/13398" && unzip -o skill.zip -d .claude/skills/spring-boot-4-conventions && rm skill.zip

Installs to .claude/skills/spring-boot-4-conventions

Activation

This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.

Spring Framework 7 / Spring Boot 4 idioms and defaults. Use when writing or reviewing controllers, services, configuration, HTTP clients, async/virtual-thread code, or anything touching Spring's programming model.
213 chars✓ has a “when” trigger

About this skill

Spring Framework 7 / Spring Boot 4 conventions

Java 25 + Spring Framework 7 + Spring Boot 4. Prefer the most modern idiom unless an ADR explains otherwise.

Defaults to apply

Package layout — by feature, not by layer

Top-level packages are bounded contexts / features, not technical layers. Inside a feature, split by visibility (api published, private impl hidden). For the private impl sub-packages, apply the following rule:

  • One class of a given type (single entity, single service, etc.) — keep it directly in the feature package (no sub-package).
  • Multiple classes of the same type — use a typed sub-package within the feature:
    • model/ — JPA entities
    • repository/ — Spring Data repositories
    • service/ — service classes / implementations
    • service/ — service implementations
  • Inside api/ — keep the controller (or service interface) at the api/ level; put all request/response DTO records in an api/dto/ sub-package.

All classes in the feature's private sub-packages (model, repository, service, internal) must be public (cross-package visibility is required when code spans multiple sub-packages). Cross-feature access is still forbidden and enforced by ArchUnit — public here means "visible within this feature", not "part of the published API".

The api/ sub-package is the only published surface. Other features depend only on <feature>.api.

com.example.checkout
├── giftcard/                    # feature/domain — one package per bounded context
│   ├── api/                     # published surface: controller, service interfaces, events
│   │   ├── GiftCardController.java
│   │   ├── GiftCardRedemptionService.java
│   │   ├── GiftCardRedeemed.java
│   │   ├── dto/                 # request/response DTOs always in api/dto/
│   │   │   ├── RedeemCommand.java
│   │   │   └── GiftCardRedemptionResponse.java
│   │   └── exception/           # domain exceptions thrown by this feature's services
│   │       └── InsufficientBalanceException.java
│   ├── model/                   # entities (multiple → typed sub-package)
│   │   ├── GiftCardEntity.java
│   │   └── GiftCardTransactionEntity.java
│   ├── repository/              # repositories (multiple → typed sub-package)
│   │   ├── GiftCardRepository.java
│   │   └── GiftCardTransactionRepository.java
│   └── service/                 # service implementations (multiple → typed sub-package)
│       └── GiftCardRedemptionServiceImpl.java
├── order/                       # another feature
│   ├── api/
│   │   └── dto/                 # DTOs always in api/dto/
│   │       └── OrderSummaryResponse.java
│   └── service/                 # single service impl — sub-package still used for consistency
│       └── OrderService.java
└── shared/                      # cross-cutting: error envelope, security config, time
    └── exception/               # GlobalExceptionHandler (@RestControllerAdvice)

Forbidden layouts (do not create these at the application root level):

com.example.checkout
├── controller/        ❌ by-layer at root
├── service/           ❌ by-layer at root
├── repository/        ❌ by-layer at root
└── model/             ❌ by-layer at root

The forbidden layouts are top-level by-layer packages. Typed sub-packages (model/, repository/, service/) are allowed — and encouraged when there are multiple classes — inside a feature/domain package.

Why:

  • Features change together; layers don't. By-feature keeps the diff for one change inside one package.
  • Typed sub-packages within a feature improve navigation when a bounded context grows beyond 4–5 private classes.
  • ArchUnit enforces that no other feature reaches into model/, repository/, or service/ sub-packages (see archunit-rules).
  • It maps 1:1 to the module boundaries enforced by archunit-rules.

Cross-feature interaction:

  • Other features depend only on <feature>.api.
  • Prefer events (ApplicationEventPublisher) for fire-and-forget integration.
  • Direct dependencies between features are explicit and asserted by ArchUnit (e.g. order may depend on giftcard.api, never the reverse).

Dependency injection

  • Constructor injection only. No field @Autowired. No setter injection except where Spring forces it (e.g., a pre-existing framework callback).
  • One final field per dependency. Write the constructor explicitly — Lombok is forbidden (see No Lombok below).
@Service
public class CheckoutService {
    private final OrderRepository orders;
    private final GiftCardClient giftCards;

    public CheckoutService(OrderRepository orders, GiftCardClient giftCards) {
        this.orders = orders;
        this.giftCards = giftCards;
    }
}

HTTP clients

  • Outbound HTTP: prefer @HttpExchange declarative clients built on RestClient. Use WebClient only when the call is genuinely reactive end-to-end.
  • Do not use RestTemplate in new code.
  • Each external API gets its own client interface in the infra package.
@HttpExchange(url = "/cards", accept = "application/json")
public interface GiftCardClient {
    @GetExchange("/{code}")
    GiftCard fetch(@PathVariable String code);
}

Web layer

  • Controllers are @RestController. Return DTO records, not entities.
  • Use Java record for request/response DTOs.
  • Validate input at the controller boundary:
    • Annotate the controller class with @Validated.
    • Annotate every @RequestBody parameter with @Valid.
    • Annotate every @PathVariable and @RequestParam with the appropriate Bean Validation constraint (@Positive, @NotBlank, @Max, etc.).
    • Always add tests that exercise each constraint; a constraint with no test is untested behaviour.
  • Never use Pageable as a controller parameter. Pageable is a Spring Data internal type; exposing it on the API surface leaks persistence concerns, makes parameters implicit to callers, and prevents OpenAPI tooling from generating correct query-param docs. Instead:
    • Declare explicit @RequestParam fields (page, size, sort params).
    • Apply Bean Validation constraints directly (@PositiveOrZero, @Positive, @Max(100)).
    • Construct PageRequest.of(page, size) inside the method and pass it to the service.
    @GetMapping
    FooPageResponse list(
        @RequestParam(defaultValue = "0") @PositiveOrZero int page,
        @RequestParam(defaultValue = "25") @Positive @Max(100) int size) {
      return fooService.getPage(PageRequest.of(page, size));
    }
    
  • Avoid ResponseEntity<T> as a return type unless varying the HTTP status code at runtime is unavoidable. For the common cases:
    • Add a response header (e.g. ETag, Location) → inject HttpServletResponse and call response.setHeader(...), then return the DTO directly.
    • 201 Created with Location → use @ResponseStatus(HttpStatus.CREATED) on the method + set the Location header via HttpServletResponse.
    • ResponseEntity is acceptable only in handlers that need to conditionally vary the status (e.g. 304 Not Modified based on If-None-Match).
  • Map exceptions via @RestControllerAdvice to a single, documented error envelope.
// in <feature>/api/dto/
public record ApplyGiftCardRequest(@NotBlank String code, @Min(0) int orderTotalCents) {}
public record ApplyGiftCardResponse(int redeemedCents, int newOrderTotalCents) {}

Persistence

  • Spring Data JPA is the default. Use @Query for non-trivial reads; never rely on derived queries longer than three predicates.
  • Always paginate list endpoints (Pageable). Reject unbounded queries.
  • Use @ServiceConnection with Testcontainers in tests instead of application-test.yml overrides.
  • Use Instant for audit timestamps (created_at, updated_at), not LocalDateTime. Instant is an absolute UTC point in time; LocalDateTime is ambiguous across DST transitions and timezone changes. Hibernate 6 maps Instant to DATETIME/TIMESTAMP columns using UTC normalization — no schema change required.

Virtual threads

  • Spring Boot 4 enables virtual threads for the web server by default. Keep it on. Do not introduce custom Executors that pin to platform threads without measuring.

Configuration

  • One @ConfigurationProperties record per bounded module. Validate with @Validated.
  • No @Value for grouped settings. Use @Value only for ad-hoc single primitives.
@ConfigurationProperties("app.giftcard")
@Validated
public record GiftCardProperties(@NotBlank String baseUrl, @Min(1) int timeoutMs) {}

Observability

  • Structured logging via Logger + key/value pairs (Boot 4 native StructuredLoggingFormatter).
  • Metrics via MeterRegistry; one counter per business outcome (gift_card.redeemed, gift_card.rejected).

AOT-friendly

  • No reflection on application code.
  • Beans are @Component, @Service, etc. at compile time. No runtime registration.
  • Anything dynamic is registered via a RuntimeHintsRegistrar and noted in the design doc.

Anti-patterns to flag in review

  • Field injection (@Autowired on a field).
  • Lombok in any form (@Data, @Getter, @Setter, @Builder, @RequiredArgsConstructor, @Slf4j, @SneakyThrows, etc.) — see No Lombok below.
  • Top-level packages named controller, service, repository, model, dto, util (by-layer layout at the application root) — use feature/domain packages instead (see Package layout). Note: model/, repository/, and service/ sub-packages within a feature package are allowed when there are multiple classes of that type.
  • RestTemplate in new code.
  • Returning entities from controllers.
  • Untyped Map<String, Object> request/response bodies.
  • Catching Exception then rethrowing RuntimeException with no message.
  • @SpringBootTest for code that a slice test (@WebMvcTest, @DataJpaTest) can cover.
  • Pageable as a controller method parameter — use

Content truncated.

Search skills

Search the agent skills registry