Java & Spring Boot Patterns
Your Spring Boot service compiles, but the AI scaffolded field injection, a leaky @Transactional boundary, and no @ControllerAdvice — and it falls over the first time two requests race on the same order. Generated Java looks idiomatic until production traffic finds the seams: N+1 queries, swallowed exceptions, a KafkaTemplate call that no longer compiles on Spring Boot 3. These recipes produce Spring Boot 3.x code on Java 21 that holds up, and — just as important — show you how to catch the failure modes the model still gets wrong.
What You Will Walk Away With
Section titled “What You Will Walk Away With”- A paste-ready prompt for a REST resource with validation, pagination, and a single global exception handler
- A JPA prompt that uses Hibernate 6.4’s managed soft-delete instead of the legacy
@Wherepattern - A resilient downstream-client prompt (Resilience4j circuit breaker + the current
CompletableFutureKafka API) - A Spring Security 6 prompt for stateless JWT auth that survives review
- A testing prompt that produces Testcontainers integration tests, not happy-path mocks
- The Spring-specific gotchas to check on every generation, plus the MCP servers that tighten the loop
The Workflow
Section titled “The Workflow”Step 1: Scaffold the project (this part differs per tool)
Section titled “Step 1: Scaffold the project (this part differs per tool)”Spring Initializr is the source of truth for the dependency tree, so the useful move is having the agent call it and then wire your conventions on top.
In agent mode, point Cursor at a fresh directory:
Generate a Spring Boot 3.4 project on Java 21 using start.spring.io withdependencies: web, data-jpa, validation, security, actuator, postgresql,flyway. Maven build. Then add a CLAUDE.md-style conventions file documentingthe rules below and apply them to the generated code.Cursor scaffolds in the workspace, shows a diff, and you accept per file. Keep the conventions in a .cursor/rules/ file so every later edit inherits them.
claude "Create a Spring Boot 3.4 project on Java 21 via start.spring.io withweb, data-jpa, validation, security, actuator, postgresql, and flyway. Maven.Write a CLAUDE.md capturing our conventions, then make the generated code follow it."Claude Code runs the curl against start.spring.io, unzips, and edits in place. The CLAUDE.md it writes is reused automatically on every subsequent run in the repo.
codex "Scaffold a Spring Boot 3.4 / Java 21 Maven project from start.spring.io(web, data-jpa, validation, security, actuator, postgresql, flyway). Add anAGENTS.md with our conventions and apply them."For larger refactors, kick this off as a Codex Cloud task on a worktree so the scaffold and your existing branch do not collide. Codex reads AGENTS.md on each task.
Whichever tool you use, give it these conventions up front — they prevent the most common review-blocking output:
- Java 21: records for DTOs, pattern matching, virtual threads where I/O-bound- Constructor injection only (never @Autowired field injection)- One @ControllerAdvice for the whole app; never catch-and-swallow- @Transactional only on service methods, readOnly=true by default- No business logic in controllers; no entities returned from controllers (DTOs only)- Every new endpoint ships with a Testcontainers integration testStep 2: Generate a REST resource
Section titled “Step 2: Generate a REST resource”Now the part that is identical across all three tools. The trick is to specify the response envelope and the failure behavior, not just “CRUD” — otherwise you get a happy-path controller that leaks entities and returns stack traces.
Then read what comes back against the conventions. The high-value check is the exception handler: models love to scatter try/catch blocks that log and return null. You want exactly one advice class and zero swallowed exceptions. A correct controller stays this thin:
@RestController@RequestMapping("/api/v1/products")class ProductController { private final ProductService products;
ProductController(ProductService products) { // constructor injection, no @Autowired this.products = products; }
@GetMapping Page<ProductResponse> list(@PageableDefault(size = 20) Pageable pageable, @RequestParam(required = false) String search) { return products.search(search, pageable); // validation + mapping live in the service }}If the generated controller is fatter than this — mapping logic, try/catch, an injected EntityManager — send it back: “Move all mapping and error handling out of the controller; rely on the @RestControllerAdvice.”
Step 3: Persistence without the legacy soft-delete trap
Section titled “Step 3: Persistence without the legacy soft-delete trap”Ask for soft delete and most models still emit Hibernate 5’s @SQLDelete + @Where pair. @Where is deprecated in Hibernate 6 (replaced by @SQLRestriction), and Spring Boot 3.2+ ships Hibernate 6.4, which has a fully managed @SoftDelete annotation. Name the version so you get the modern path.
The managed annotation collapses the old two-annotation dance to one line, and Hibernate filters soft-deleted rows everywhere automatically:
@Entity@Table(name = "orders")@SoftDelete // Hibernate 6.4+: replaces @SQLDelete + @Where, applied to every query@EntityListeners(AuditingEntityListener.class)class Order { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
@ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "customer_id", nullable = false) private Customer customer; // ... items, status, auditing fields}Step 4: Resilient downstream calls
Section titled “Step 4: Resilient downstream calls”A microservice that calls payment or inventory synchronously needs a circuit breaker, and any code touching Kafka must use the current API. On Spring Kafka 3.x (Spring Boot 3.x), KafkaTemplate.send() returns a CompletableFuture — the old ListenableFuture.addCallback(...) no longer exists and will not compile.
The Kafka publisher should look like this — whenComplete, not addCallback:
@Componentclass OrderEventPublisher { private final KafkaTemplate<String, OrderEvent> kafkaTemplate;
OrderEventPublisher(KafkaTemplate<String, OrderEvent> kafkaTemplate) { this.kafkaTemplate = kafkaTemplate; }
@EventListener void onOrderCreated(OrderCreatedEvent event) { var kafkaEvent = OrderEvent.from(event); // Spring Kafka 3.x returns CompletableFuture; .addCallback was removed kafkaTemplate.send("order-events", kafkaEvent).whenComplete((result, ex) -> { if (ex == null) log.info("Event sent: {}", kafkaEvent.eventId()); else log.error("Failed to send event {}", kafkaEvent.eventId(), ex); }); }}Step 5: Stateless JWT security
Section titled “Step 5: Stateless JWT security”Spring Security 6 dropped WebSecurityConfigurerAdapter and the old DSL. Pin the version so the agent produces the lambda DSL with a SecurityFilterChain bean rather than resurrecting the deprecated base class.
Step 6: Tests that prove behavior, not coverage theater
Section titled “Step 6: Tests that prove behavior, not coverage theater”The reason to insist on Testcontainers in the conventions is that mocked-repository tests pass while the real query is broken. A generation is not done until it ships an integration test that exercises a real database.
When This Breaks
Section titled “When This Breaks”What’s Next
Section titled “What’s Next”- Database patterns for deeper JPA, query tuning, and migration workflows
- API patterns for cross-language REST and error-envelope design
- ORM patterns for the N+1 and lazy-loading playbook across ORMs
- Kubernetes patterns for deploying these services to a cluster