Skip to content

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.

  • 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 @Where pattern
  • A resilient downstream-client prompt (Resilience4j circuit breaker + the current CompletableFuture Kafka 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

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 with
dependencies: web, data-jpa, validation, security, actuator, postgresql,
flyway. Maven build. Then add a CLAUDE.md-style conventions file documenting
the 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.

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 test

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
}

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:

@Component
class 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);
});
}
}

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.