
Introduction
- As developers, we’ve all been there: you prompt an AI coding assistant to write a function, and it spits out code using a framework version from three years ago, ignores your project’s naming conventions, and forgets to handle basic errors. You spend more time refactoring the AI’s output than it would have taken to write the code from scratch.
- But what if you could train your AI editor to think, code, and architect exactly like a senior developer on your specific team?
- If you are using Cursor, you have a superpower at your fingertips. By effectively configuring Rules, Skills, and the Knowledge Base, you can transform Cursor from a generic LLM wrapper into a highly tailored, deeply contextual extension of your own brain.
- Here is how to master the “big three” configurations in Cursor.
The Foundation: Crafting the Perfect Rule
- Instead of relying on a single monolithic config file, modern versions of Cursor allow you to create modular, project-specific rule files inside a
.cursor/rules/directory. Creating a foundational file like.cursor/rules/cursor-rules.mdis your project’s constitution. - The biggest mistake developers make is writing a massive, unstructured wall of text. Cursor will suffer from “context drift” if you overwhelm it. Instead, use a strict, bulleted Markdown structure.
A Bulletproof cursor-rules.md Template
---
description: Global coding standards and architectural rules for this Java/Spring Boot repository
globs: "**/*.java"
alwaysApply: false
---
# Project Context
- Tech Stack: Java 21, Spring Boot 3.x, Spring Security, Hibernate/JPA.
- Build Tool & Database: Maven, PostgreSQL.
# Code Style & Conventions
- Use modern Java features where applicable (e.g., Records for DTOs, Pattern Matching, Switch Expressions).
- Adhere strictly to Google Java Style Guide (4-space indents).
- Prefer constructor injection over `@Autowired` on fields.
- Keep controllers thin; delegate all business logic to `@Service` classes.
# Error Handling & Resilience
- Never swallow exceptions. Always catch specific exceptions, log them, or rethrow them.
- Use a localized `@ControllerAdvice` and `@ExceptionHandler` for global REST API error handling.
- Use `Optional<T>` instead of returning `null` to avoid `NullPointerException`.
- Log errors using SLF4J: `log.error("Context message", e)`, never `System.out.println()` or `e.printStackTrace()`.
# Architecture Rules
- Follow Domain-Driven Design (DDD) layout: controller, service, repository, entity, dto.
- Ensure all entity classes have properly defined `equals()` and `hashCode()` methods using business keys.
Pro-Tip: Keep it concise. Focus only on the rules that you find yourself repeatedly correcting Cursor on. If it already writes decent standard Java syntax, don’t waste context space telling it basics. Focus on your architectural preferences.
Expanding Capabilities: Custom Skills & Prompt Engineering
- While
cursor-rules.mddictates how the code should look, Skills (often managed via system prompts or reusable custom prompts) dictate how Cursor behaves during specific workflows. - Think of Skills as shortcuts for repetitive, complex developer tasks. Instead of typing a long prompt every time you want a test written, define a rigid workflow.
Example Workflow: The “TDD Unit Tester” Skill
When asking Cursor to write tests in the chat or composer, instruct it to follow a specific operational sequence. You can save this template in your notes or a separate local rule file to feed to Cursor when starting a feature:
“Act as a Senior QA Automation Engineer. When I provide a Java service class, you must generate a JUnit 5 unit test file using Mockito following these steps:
- List the critical edge cases (e.g., empty optionals, database connection drops, invalid DTO payloads).
- Set up the test class with
@ExtendWith(MockitoExtension.class).- Use
@Mockfor dependencies and@InjectMocksfor the target service.- Ensure all assertions use AssertJ (
assertThat(...)) for fluent assertions.- Aim for 100% mutation testing coverage boundaries.”
Save the following block as .cursor/skills/unit-tester/ in your project to give Cursor an automated test-generation workflow whenever you open or create a file ending in SKILL.mdTest.java.
---
description: Trigger this rule when generating or refactoring JUnit 5 unit tests for Java services
globs: "**/*Test.java"
---
# Role: Senior QA Automation Engineer (TDD & Unit Testing Specialist)
You are an expert QA Automation Engineer specializing in Java, Spring Boot, JUnit 5, and Mockito. Your goal is to write bulletproof, highly resilient unit tests using AssertJ for fluent assertions.
## Execution Workflow
When the user asks you to write tests for a Java class, you must strictly follow this 4-step sequence. Do not skip any steps.
### Step 1: Edge Case Analysis
Before writing any code, list out the critical edge cases and boundary conditions for the target service in a markdown comment. Consider:
- `Optional.empty()` handling.
- `null` or invalid DTO input payloads.
- Database entity omissions (e.g., entity not found throws `ResourceNotFoundException`).
- Network or external service communication drops (simulated via Mockito stubs throwing exceptions).
### Step 2: Test Environment Boilerplate
Set up the test class exactly using these technical specs:
- Use `@ExtendWith(MockitoExtension.class)` instead of manual mocks initialization.
- Use `@Mock` for all downstream dependencies.
- Use `@InjectMocks` to instantiate the service under test.
- Do not use `@SpringBootTest` unless explicitly requested (keep tests fast and isolated).
### Step 3: Test Implementation Structure
- Structure test methods using the **Given-When-Then** (AAA - Arrange, Act, Assert) format.
- Use descriptive snake_case or camelCase names that explicitly state the expected outcome (e.g., `shouldThrowException_WhenUserNotFound`).
- Always use **AssertJ** for assertions (e.g., `assertThat(result).isNotNull();`).
### Step 4: Verification Checklist
Review your generated code against this checklist before completing the response:
- Are all mocks stubbed using `when(...).thenReturn(...)` or `doThrow(...)` properly?
- Is there an explicit verification (`verify(dependency, times(1)).method()`) for critical side effects?
- Did you avoid using `System.out.println()` inside the tests?
---
## Code Example Layout
```java
package com.example.service;
import com.example.exception.ResourceNotFoundException;
import com.example.model.User;
import com.example.repository.UserRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
@DisplayName("Should successfully return user when valid ID is provided")
void shouldReturnUser_WhenIdIsValid() {
// Given
Long userId = 1L;
User mockUser = new User(userId, "John Doe");
when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser));
// When
User result = userService.getUserById(userId);
// Then
assertThat(result).isNotNull();
assertThat(result.getName()).isEqualTo("John Doe");
verify(userRepository, times(1)).findById(userId);
}
@Test
@DisplayName("Should throw ResourceNotFoundException when user does not exist")
void shouldThrowException_WhenUserDoesNotExist() {
// Given
Long userId = 99L;
when(userRepository.findById(userId)).thenReturn(Optional.empty());
// When & Then
assertThatThrownBy(() -> userService.getUserById(userId))
.isInstanceOf(ResourceNotFoundException.class)
.hasMessageContaining("User not found with id: " + userId);
verify(userRepository, times(1)).findById(userId);
}
}
By teaching Cursor specific roles and workflows, you drastically improve the logical depth of its output.
Feeding the AI: Building a Powerful Knowledge Base
- An AI is only as smart as the data it can access. Out of the box, LLMs might not know about the absolute latest Spring Boot patch notes or your company’s proprietary internal Java libraries.
- Cursor solves this through its Knowledge Base features—specifically documentation indexing and local knowledge files.
Indexing External Docs
Don’t let Cursor guess how a specific library works. Go to Settings > Indexings & Docs > Add Docs in Cursor and add the URL of the documentation you use heavily (e.g., https://docs.spring.io/spring-boot/docs/current/reference/html/). Cursor will crawl and index it, allowing you to use @SpringBoot in your chats to get flawlessly accurate, up-to-date syntax.
Creating a Local INTERNAL_DOCS.md
If your enterprise project has complex business logic (e.g., “How our custom OAuth2 multi-tenant filter chain works”), write it down in a markdown file within your repository. For example:
---
description: Internal architecture and business logic guide for the core system
globs: "**/service/*.java, **/config/*.java"
---
# Enterprise Project System Architecture & Knowledge Base
This document serves as the core source of truth for the application's unique architectural patterns, multi-tenant routing, and business logic invariants. Use this context whenever refactoring or generating service and security components.
---
## 1. Global Multi-Tenancy Architecture
Our system enforces hard data isolation across business clients using a **Multi-Tenant Discriminator Column** approach managed via Hibernate filters.
### Context & Flow
- Every incoming HTTP request must include an `X-Tenant-ID` header.
- The `TenantInterceptor` extracts this header and stores it in a thread-local context via `TenantContextHolder`.
- Every JPA entity that is tenant-aware extends `BaseTenantEntity`.
### Rule for Code Generation:
When querying the database inside custom native SQL or repository layer implementations, **never** manually append `WHERE tenant_id = ...`. The application relies entirely on Hibernate's global `@Filter` mechanism which auto-injects this condition.
---
## 2. Security Filter Chain Configuration
Our application implements a custom OAuth2 and Stateful Session hybrid token validation pipeline inside the security configuration (`WebSecurityConfigurerAdapter` / `SecurityFilterChain`).
When you need Cursor to touch that code, simply type:
Refactor the authentication filter using the architecture defined in
@INTERNAL_DOCS.md.
My Personal Lessons Learned
After months of tweaking my Cursor setup across multiple production enterprise repositories, I’ve moved past basic configurations. Here are my top advanced, battle-tested strategies for mastering Cursor:
Stop Manual Editing—Force the AI to Learn From Mistakes
- When Cursor generates code that violates your project’s standards or contains a bug, do not fix it manually in your editor. Instead, hit
Cmd/Ctrl + LorCmd/Ctrl + I, point out the exact mistake, and make Cursor fix its own code. - By forcing the AI to correct itself through dialogue, it “remembers” the context within that session. More importantly, it helps you clearly identify what specific rules you need to extract and add to your permanent rule files.
Let Cursor Generate Its Own Skills (The Meta-Prompting Loop)
Writing custom .md skill files from scratch can be tedious. Instead, leverage Cursor to build them dynamically.
- First, ask Cursor to perform a complex task for you sequentially (e.g., “Help me refactor this legacy controller step-by-step”).
- Provide live feedback and guide it until the output is absolutely flawless.
- Finally, hit it with a meta-prompt: “Based on the workflow and corrections we just did, generate a structured markdown file for a new Cursor Skill so you can repeat this exact process perfectly next time.” Copy, paste, done.
Use Companion Templates for Complex, Structured Skills
- For advanced skills that generate highly structured outputs (such as API contracts, database schemas, or complex configurations), do not just rely on text explanations. Create a dedicated template or schema file inside your
.cursor/skills/ordocs/templates/folder. - In your skill definition file, explicitly reference that template. For example, tell the skill: “Always adhere to the JSON schema defined in
@api-response-schema.json.” This ensures structural conformity that raw text prompting alone often misses.
Less is More
If your cursor-rules.md file is longer than two pages, Cursor will start prioritizing the rules at the top and ignoring the ones at the bottom. Keep it punchy.
The @ Symbol is Your Best Friend
Don’t just type a prompt blindly. Explicitly guide the AI’s attention by tagging @Files, @Folders, or @Docs. Garbage in, garbage out; precise context in, perfect Java code out.
Keep it Modular and Sync with Git
Commit your entire .cursor/ directory to your Git repository. This ensures that every backend developer on your team inherits the exact same AI coding standards, skills, and templates automatically.
Conclusion
Setting up Cursor properly takes about 15 minutes, but it will save you hundreds of hours of debugging and tedious code-correction down the line. By establishing clear Markdown Rules, defining structured Skills, and feeding it a robust Knowledge Base, you turn Cursor from a novelty assistant into the most efficient engineer on your backend team.
What does your cursor-rules.md look like for Java development? Drop your favorite AI-steering tips in the comments below!
Note:
The above results are only based on the author’s personal experience and testing, depending on the case, the results may be different. In addition, AI is constantly developing, so in the future, the recommendations may no longer be correct.
Although the above experiences are illustrated with Cursor AI, you can still apply similar principles to other AI tools such as GitHub Copilot, Claude, Jetbrains AI, etc. Each tool may require some minor adjustments to suit its specific needs.