·10 min read

Cursor Rules for Spring Boot: Java AI Coding Guide

Write Cursor rules for Spring Boot that stop AI layering mistakes, JPA pitfalls, and flaky tests — with a copyable rule set for Cursor, Copilot, and Claude Code.

Spring Boot has more convention baked into it than almost any other backend framework, and AI coding tools trip over those conventions constantly. Cursor rules for Spring Boot fix this: a project-level rules file (.cursor/rules/*.mdc) that pins down your layering, dependency injection style, JPA usage, and testing strategy so the model stops guessing. Instead of reviewing field-injected @Autowired beans and javax.* imports in every diff, you state your conventions once and every generation follows them.

This guide covers the specific rules a Spring Boot codebase needs: where AI defaults go wrong, layering rules for controllers, services, repositories, and DTOs, JPA and Hibernate safety, and test slices with Testcontainers. It ends with a copyable rule set that works as Java Cursor rules, Copilot instructions, or a CLAUDE.md — the content is identical, only the file location changes.

Where Spring Boot AI coding goes wrong

Spring Boot AI coding fails in predictable ways because training data is full of tutorial-era Spring. The model writes code that compiles and looks plausible, but it is the Spring of 2019, not the Spring Boot 3.x project you actually maintain. The recurring offenders:

  1. Field injection. Models love @Autowired on private fields because a decade of blog posts used it. Constructor injection is the standard: it makes dependencies explicit, enables final fields, and keeps beans testable without reflection tricks.
  2. javax.* imports. Spring Boot 3 moved to Jakarta EE namespaces. AI tools still regularly emit javax.persistence.Entity and javax.validation.constraints.NotNull, which fail to compile against jakarta.* dependencies.
  3. Entities leaking through controllers. The quickest demo returns @Entity classes straight from REST endpoints. In a real project that serializes lazy proxies, exposes internal columns, and couples your API contract to your schema.
  4. Deprecated security config. WebSecurityConfigurerAdapter was removed in Spring Security 6, yet models keep extending it. You want SecurityFilterChain beans, and the AI will not pick that unless told.
  5. @Transactional in the wrong place. Sprinkled on controllers, on private methods where it silently does nothing, or missing from multi-write service methods entirely.
  6. @SpringBootTest for everything. The full application context boots for a test that exercises one repository method, and your suite takes eight minutes.

None of these are exotic. They are defaults, and defaults are exactly what rules files exist to override. If you have not written a rules file before, the Cursor rules guide covers the mechanics; the rest of this post covers what to put in one for Spring Boot.

Cursor rules for Spring Boot layering: controllers, services, repositories, DTOs

Layering is the highest-leverage section in any Cursor rules for Spring Boot because a single generated endpoint touches every layer. Spell out what each layer owns and — just as important — what it must not do:

## Layering

- Controllers are thin: validate input, delegate to a service, map to a response.
  No business logic, no repository access, no @Transactional in controllers.
- Services own business logic and transaction boundaries. Public service methods
  that write data are annotated @Transactional.
- Repositories are Spring Data JPA interfaces. No JPQL in services or controllers;
  query logic lives in the repository.
- Never return @Entity classes from controllers. Every endpoint uses request and
  response DTOs, written as Java records.
- Map entity <-> DTO in dedicated mapper classes (or MapStruct), not inline in
  controllers.
- Use constructor injection only. No @Autowired on fields or setters. Declare
  dependencies as private final fields.

With those rules active, a generated endpoint comes out the way you would write it:

@RestController
@RequestMapping("/api/orders")
public class OrderController {

    private final OrderService orderService;

    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }

    @PostMapping
    public ResponseEntity<OrderResponse> create(@Valid @RequestBody CreateOrderRequest request) {
        OrderResponse response = orderService.createOrder(request);
        return ResponseEntity.status(HttpStatus.CREATED).body(response);
    }
}

And the DTOs stay boring, which is the point:

public record CreateOrderRequest(
    @NotNull Long customerId,
    @NotEmpty List<OrderLineRequest> lines
) {}

public record OrderResponse(Long id, String status, BigDecimal total) {}

Records as DTOs is worth stating explicitly. Without it, models produce mutable classes with Lombok annotations you may not even have on the classpath. If your team does use Lombok, say so in the rules; if not, say that too — "Lombok is not used in this project" prevents an entire category of broken imports. The same principle applies in other stacks — the TypeScript backend rules post walks through the equivalent layering decisions for Node services.

JPA and Hibernate safety rules

JPA is where AI-generated Java gets expensive. The code works in the demo, then produces an N+1 query storm or a LazyInitializationException in production. These rules target the failure modes:

## JPA and Hibernate

- All @ManyToOne and @OneToOne associations are FetchType.LAZY. Never use EAGER.
- Solve N+1 with fetch joins or @EntityGraph on the repository method that needs
  the association — not by switching the mapping to EAGER.
- spring.jpa.open-in-view is false. Load everything a request needs inside the
  service's transaction; never rely on lazy loading in controllers or serializers.
- Read-only service methods use @Transactional(readOnly = true).
- @Transactional goes on public service methods only. It does not work on
  private methods or self-invoked calls.
- Do not override equals/hashCode on entities using the generated ID. If needed,
  use a natural key or leave them unimplemented.
- Use explicit column and table names in @Column/@Table. Never let ddl-auto
  manage production schemas; migrations go through Flyway.
- Import from jakarta.persistence, never javax.persistence.

Two of these deserve emphasis. First, disabling open-session-in-view is a real architectural decision: with it off, lazy loading outside a transaction throws instead of silently issuing queries during JSON serialization. Your rules must say where data loading happens so generated code fetches associations inside the service. Second, the @EntityGraph rule gives the model a correct escape hatch. If you only say "never EAGER," it will hit the N+1 problem and solve it badly. Give the pattern you want:

public interface OrderRepository extends JpaRepository<Order, Long> {

    @EntityGraph(attributePaths = {"lines", "customer"})
    Optional<Order> findWithDetailsById(Long id);
}

Testing rules: slices and Testcontainers

Left alone, AI assistants write @SpringBootTest with mocked-out everything — slow to boot and testing very little. Rules should force test slices and real infrastructure:

## Testing

- Unit test services with plain JUnit 5 + Mockito. No Spring context.
- Controller tests use @WebMvcTest with MockMvc and a mocked service layer.
- Repository tests use @DataJpaTest against a Testcontainers PostgreSQL
  container — never H2. Disable the embedded-database replacement.
- @SpringBootTest is reserved for a small number of end-to-end tests.
- Test names follow methodName_condition_expectedResult.
- Use @ServiceConnection for container wiring, not manual property overrides.

The H2 rule matters more than it looks. H2 accepts SQL that PostgreSQL rejects and vice versa, so repository tests against H2 verify a database you do not run. Testcontainers with @ServiceConnection makes the real thing nearly as convenient:

@DataJpaTest
@Testcontainers
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class OrderRepositoryTest {

    @Container
    @ServiceConnection
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");

    @Autowired
    OrderRepository orderRepository;

    @Test
    void findWithDetailsById_existingOrder_loadsLinesWithoutExtraQueries() {
        // ...
    }
}

Setting up Cursor rules for Spring Boot across every tool

Most Java teams run more than one assistant — Cursor in the editor, GitHub Copilot for some teammates, Claude Code in the terminal. Each reads a different file, but the rule content above is identical. Here is the setup, step by step:

  1. Cursor. Create .cursor/rules/spring-boot.mdc with frontmatter that scopes it to Java files:

    ---
    description: Spring Boot layering, JPA, and testing conventions
    globs: ["**/*.java", "**/build.gradle.kts", "**/pom.xml"]
    alwaysApply: true
    ---
    
    (paste the rule set below)
  2. GitHub Copilot. Copilot instructions for Java repos live in .github/copilot-instructions.md — plain markdown, no frontmatter. Paste the same rule set. For large repos you can split path-scoped files under .github/instructions/, but one file is fine to start.

  3. Claude Code. For a Spring Boot Claude Code setup, put the rules in CLAUDE.md at the repository root. Claude Code loads it automatically at the start of every session, so your layering and JPA rules apply to agentic multi-file edits too — which is exactly where an unguided model does the most damage.

  4. Windsurf and others. Windsurf reads .windsurf/rules/, Cline and Aider have their own conventions. Same content, different paths.

Maintaining four copies of one rule set by hand is the failure mode here — they drift, and each tool ends up enforcing a different vintage of your conventions. This is the problem localskills.sh solves: publish the rule set once as a skill, and the CLI writes each tool-native format from that single source:

localskills install your-team/spring-boot-rules --target cursor claude windsurf

That one command produces the .cursor/rules file, the .claude/skills/ entry, and the .windsurf/rules/ file from the same published version, with rollback if an update misfires.

The copyable rule set

Here is the full starter set, assembled from the sections above. Trim it to your stack — a rules file only earns trust if every line is true for your project, a principle covered in depth in AI coding rules best practices.

# Spring Boot project rules

## Stack
- Java 21, Spring Boot 3.x, Gradle (Kotlin DSL), PostgreSQL, Flyway
- Jakarta namespaces only: import jakarta.*, never javax.*
- Lombok is not used. Write constructors by hand; DTOs are Java records.

## Layering
- Controllers: validate input, delegate to services, map to response DTOs.
  No business logic, repository access, or @Transactional in controllers.
- Services own business logic and transaction boundaries.
- Repositories are Spring Data JPA interfaces; query logic lives there.
- Never return @Entity classes from controllers. Use request/response records.
- Constructor injection only; dependencies are private final fields.

## Spring Security
- Configure security with SecurityFilterChain beans.
- Never extend WebSecurityConfigurerAdapter (removed in Spring Security 6).

## JPA and Hibernate
- All associations are FetchType.LAZY; never EAGER.
- Fix N+1 with fetch joins or @EntityGraph on repository methods.
- spring.jpa.open-in-view=false. Load all needed data inside the service
  transaction; no lazy loading in controllers.
- @Transactional on public service methods only; readOnly=true for queries.
- No equals/hashCode on entities based on generated IDs.
- Schema changes go through Flyway migrations, never ddl-auto.

## Testing
- Services: plain JUnit 5 + Mockito, no Spring context.
- Controllers: @WebMvcTest + MockMvc.
- Repositories: @DataJpaTest + Testcontainers PostgreSQL (never H2),
  wired with @ServiceConnection.
- @SpringBootTest only for a handful of end-to-end tests.
- Test names: methodName_condition_expectedResult.

## Error handling
- One @RestControllerAdvice with @ExceptionHandler methods returning
  ProblemDetail responses. No try/catch blocks in controllers.

Start with this, delete what does not apply, and add the conventions your last three code reviews kept repeating. That review-comment archive is the best source of rules you have: anything you have typed twice as PR feedback belongs in the file so the model stops making the mistake in the first place.


Ready to share one Spring Boot rule set across Cursor, Copilot, Claude Code, and the rest of your team's tools? Create a free localskills.sh account and publish it as a versioned skill.

npm install -g @localskills/cli
localskills login
localskills publish