Code review is the most effective quality gate in software development — but it is also the most time-constrained. Reviewers scan for obvious bugs and style issues, but subtle problems like carrier pinning inside synchronized blocks, hidden N+1 queries in service layers, and resource leaks in non-obvious code paths regularly slip through. I built these prompts after a quarter where our team’s post-incident reviews kept surfacing the same categories of bug: thread-safety assumptions broken by virtual threads, transaction boundaries that worked in tests but silently failed under concurrent load, and SOLID violations that only became painful when the code needed to be extended. The pattern was clear — these were not one-off mistakes, they were the class of problem that human reviewers consistently miss because they require holding the entire call graph in working memory at once. AI assistants handle this better. These 10 prompts are structured to extract that analysis reliably.
This post gives you 10 AI prompts engineered for Java code quality review. Each prompt targets a specific class of problem, provides a structured diagnosis checklist, and requests actionable fixes rather than just observations. Use them before a pull request, as part of a legacy codebase audit, or as a learning tool when onboarding to an unfamiliar module.
For complementary content, see 10 AI Prompts to Generate JUnit 6 Tests and Virtual Threads vs Platform Threads — Benchmarks and Code.
How to Get Actionable Output, Not Generic Advice
The difference between a useful code review prompt and a useless one is specificity of input. AI assistants give generic advice when given generic code. Paste the full class — not excerpts — so the AI can see field declarations, constructor dependencies, and method interactions. A method that looks fine in isolation often reveals a threading issue when the AI sees that the field it reads is mutable and shared. For large classes (>300 lines), split by responsibility first. “Review the transaction boundary logic in these 3 methods” gets better targeted output than “review this 800-line service.” Tell the AI what you already suspect — it produces more precise findings with a hypothesis to test.
Prompt 1: SOLID Principle Audit
When to use: A class has grown organically over many months and you suspect it is violating one or more SOLID principles — but you want a structured analysis before proposing a refactoring that teammates will resist without clear justification.
Audit the following Java class for SOLID principle violations and propose concrete refactoring steps.
Class to audit:
[PASTE YOUR CLASS HERE]
For each of the five SOLID principles, evaluate whether the class violates it:
S — Single Responsibility Principle:
- Does this class have more than one reason to change?
- List every distinct responsibility you can identify (e.g., "validates input", "persists to DB", "sends email")
- If multiple responsibilities exist, propose how to split the class
O — Open/Closed Principle:
- Are there switch/if-else chains on a type or status field that would require modifying this class to add a new case?
- If so, propose a Strategy or Visitor pattern refactoring
L — Liskov Substitution Principle:
- If this class has subclasses or implements an interface, does it honour the contract of its supertype?
- Flag any method that throws exceptions the supertype does not declare, or weakens preconditions
I — Interface Segregation Principle:
- Does this class implement an interface with methods it does not use or implement as no-ops?
- If so, propose splitting the interface
D — Dependency Inversion Principle:
- Does this class depend directly on concrete classes (using new) instead of abstractions (interfaces)?
- List every concrete dependency and flag which ones should be injected
Output format:
- Violation: [Principle] — [one-line description]
- Evidence: [specific line numbers or method names]
- Refactoring: [concrete suggestion with before/after code snippet]
- Severity: HIGH (causes real maintenance pain) / MEDIUM / LOW (style only)
Prompt 2: Virtual Thread Carrier Pinning Audit
When to use: You are migrating to virtual threads (Java 21+) or reviewing code that will run on a virtual-thread executor, and you want to find every synchronized block that wraps a blocking operation — the primary cause of carrier thread pinning that eliminates virtual thread scalability benefits.
Audit the following Java class for virtual thread carrier pinning risks (Java 21+).
Class to audit:
[PASTE YOUR CLASS HERE]
Background: A virtual thread is pinned to its carrier (platform) thread when it blocks inside a synchronized block or synchronized method. On Java 21-24, this prevents the carrier from being reused by other virtual threads and eliminates scalability benefits. Java 25 resolves most synchronized pinning.
Audit checklist — identify every instance of:
1. synchronized block or synchronized method that contains:
- Thread.sleep()
- Any blocking I/O (InputStream.read, Socket operations, file reads)
- JDBC calls (Connection, PreparedStatement, ResultSet)
- HTTP calls (HttpClient.send(), RestTemplate, WebClient.block())
- Lock.lock() inside synchronized (potential deadlock AND pinning)
2. For each identified case, classify:
- CRITICAL: synchronized wraps a known blocking I/O call — must fix before virtual thread migration
- HIGH: synchronized wraps a potentially slow operation (cache lookup, internal queue)
- LOW: synchronized wraps pure in-memory computation (generally safe, minor performance cost)
3. For each CRITICAL or HIGH finding, provide the fix:
- Replace synchronized with ReentrantLock (allows virtual thread unmounting while waiting)
- Show the complete before/after code
- Explain why ReentrantLock does not pin the carrier
4. Flag any ThreadLocal fields that hold large objects (connection, session, parsed config)
- Large ThreadLocal values become a memory pressure source at virtual thread scale (millions of threads)
- Propose ScopedValue replacement for request-scoped data
See also: https://ankurm.com/virtual-threads-vs-platform-threads-benchmarks/
Prompt 3: N+1 Query Pattern Detection
When to use: A service or repository method is slow under load and you suspect N+1 query patterns — where one query loads a list of entities, then a separate query executes for each entity to load a related collection, producing hundreds of database round-trips for a single API call.
Detect and fix N+1 query patterns in the following Java service and repository code.
Service class:
[PASTE YOUR SERVICE CLASS HERE]
Repository interface / DAO class:
[PASTE YOUR REPOSITORY HERE]
Entity classes involved:
[PASTE RELEVANT ENTITY CLASSES HERE]
Audit for N+1 patterns in these scenarios:
1. Collection traversal after findAll / findBy:
- Does any code call a lazy-loaded collection (e.g., order.getItems()) inside a loop?
- Each iteration triggers an additional SELECT — classic N+1
2. @OneToMany / @ManyToMany with FetchType.LAZY (the safe default) accessed outside a transaction:
- Will throw LazyInitializationException OR trigger a new session per access (N queries)
3. Custom repository methods that return entities, followed by service code that accesses associations:
- findAllByStatus("ACTIVE") returns 500 orders → service calls order.getCustomer() 500 times
For each N+1 found:
- Show the problematic code path
- Provide the JPQL fix using JOIN FETCH: SELECT o FROM Order o JOIN FETCH o.items WHERE o.status = :status
- Show the alternative: @EntityGraph(attributePaths = {"items", "items.product"}) on the repository method
- Explain the trade-off: JOIN FETCH multiplies result rows for multiple collections (use @BatchSize instead for >1 collection)
- Show the @BatchSize fix for cases where JOIN FETCH is inappropriate
Also check:
- spring.jpa.open-in-view=true in application.yml (enables N+1 by keeping session open through view rendering — flag if found)
- Hibernate statistics logging config to detect N+1 in tests: spring.jpa.properties.hibernate.generate_statistics=true
Prompt 4: Exception Handling Audit
When to use: A codebase has grown organically and exception handling is inconsistent — some methods swallow exceptions silently, others catch Exception broadly, and error messages lack the context needed to diagnose production incidents.
Audit and fix the exception handling in the following Java class.
Class to audit:
[PASTE YOUR CLASS HERE]
Anti-patterns to find and fix:
1. Empty catch blocks — catch (Exception e) {} with no logging or rethrow
Fix: At minimum, log.error("Unexpected failure in [method]: {}", e.getMessage(), e) and rethrow or wrap
2. Catching Exception or Throwable too broadly:
- Is the broad catch necessary, or can you catch a specific subtype?
- Broad catches hide programming errors (NullPointerException, ClassCastException) that should propagate
3. Swallowing checked exceptions by converting to runtime without preserving the cause:
- throw new RuntimeException("failed") ← loses the original stack trace
Fix: throw new ServiceException("Order creation failed", e) ← preserves cause chain
4. Logging AND rethrowing the same exception (causes duplicate log entries):
- catch (Exception e) { log.error("failed", e); throw e; }
Fix: Either log OR rethrow — not both. Log at the boundary where you handle it.
5. Using exceptions for flow control:
- try { return Integer.parseInt(s); } catch (NumberFormatException e) { return 0; }
Fix: Use StringUtils.isNumeric(s) or a dedicated validator before parsing
6. Missing finally / try-with-resources for resource cleanup:
- InputStream opened but not in try-with-resources
Fix: Rewrite as try (InputStream is = ...) { ... }
7. Catch blocks that log at ERROR for expected business conditions (e.g., "user not found"):
Fix: Log at WARN with context; reserve ERROR for genuinely unexpected system failures
For each finding: show line number, anti-pattern name, before code, after code, and the production risk of the original pattern.
Prompt 5: Thread-Safety and Concurrency Bug Detection
When to use: A class is shared between threads — a Spring singleton service, a cached object, a background task — and you want to identify race conditions and visibility issues before they manifest as intermittent production bugs that are nearly impossible to reproduce.
Audit the following Java class for thread-safety and concurrency bugs.
Class to audit:
[PASTE YOUR CLASS HERE]
Context: [DESCRIBE HOW THIS CLASS IS USED, e.g. "Spring @Service singleton shared by all HTTP request threads", "Runnable submitted to a thread pool", "static utility class"]
Concurrency bug checklist:
1. Mutable shared state without synchronization:
- Non-final fields modified after construction
- Collections (HashMap, ArrayList) modified from multiple threads without synchronization
Fix: Use ConcurrentHashMap, CopyOnWriteArrayList, or explicit locking
2. Check-then-act race conditions:
- if (!map.containsKey(k)) { map.put(k, v); } — another thread may insert between check and act
Fix: Use map.putIfAbsent(k, v) or computeIfAbsent
3. Compound operations on atomic variables:
- if (counter.get() > 0) { counter.decrementAndGet(); } — not atomic as a unit
Fix: Use counter.updateAndGet(v -> v > 0 ? v - 1 : 0) or explicit lock
4. Visibility issues:
- Fields read/written from multiple threads without volatile or synchronization
- Stale values visible due to CPU cache not flushing to main memory
5. Broken singleton pattern (double-checked locking without volatile):
- private static Config instance; ... if (instance == null) { synchronized(Config.class) { if (instance == null) { instance = new Config(); } } }
Fix: Add volatile to the field declaration
6. @NotThreadSafe classes used as Spring singletons:
- SimpleDateFormat, Calendar — not thread-safe, must not be shared
Fix: Use DateTimeFormatter (immutable and thread-safe)
7. Mutable objects returned from getters of shared state:
- getItems() returns the internal list directly — callers can modify it
Fix: Return Collections.unmodifiableList(items) or a defensive copy
For each finding: severity (CRITICAL/HIGH/LOW), affected line numbers, root cause, and corrected code.
Prompt 6: Resource Leak Detection
When to use: Memory usage or file descriptor counts grow over time under load — a classic sign of resource leaks. You want to systematically audit a class for unclosed streams, connections, and other AutoCloseable resources before running a load test or going to production.
Detect and fix all resource leaks in the following Java class.
Class to audit:
[PASTE YOUR CLASS HERE]
Resource types to check:
1. java.io.* streams (InputStream, OutputStream, Reader, Writer, BufferedReader):
- Opened but not in try-with-resources
- Opened in try block but close() called in finally (correct but verbose — rewrite as try-with-resources)
- Wrapped streams: if outer stream creation throws, inner stream leaks
e.g. new BufferedReader(new FileReader(path)) — FileReader may close but BufferedReader may not
2. JDBC resources (Connection, PreparedStatement, ResultSet):
- Not closed after use
- Closed in finally but not null-checked first (NullPointerException masks original exception)
Fix: try (Connection conn = ds.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ... }
3. HTTP connections (HttpURLConnection, Apache HttpClient response, OkHttp ResponseBody):
- ResponseBody not closed after reading → connection pool exhaustion
Fix: Wrap response body consumption in try-with-resources
4. Locks not released:
- lock.lock() called but unlock() not in finally block
Fix: lock.lock(); try { ... } finally { lock.unlock(); }
5. ExecutorService not shut down:
- ExecutorService created inside a method or class but shutdownNow() never called
Fix: Use try-with-resources (Java 19+ ExecutorService implements AutoCloseable) or register a shutdown hook
6. Temporary files not deleted:
- Files.createTempFile() called but file not deleted after use
Fix: Register deletion with File.deleteOnExit() or use @TempDir in tests
For each leak: file path, resource type, leak scenario (normal path / exception path), and the corrected try-with-resources code.
Prompt 7: Null Safety and Optional Usage Audit
When to use: NullPointerException is still one of the most common production failures in Java codebases. You want to audit a class for unsafe null handling and replace ad-hoc null checks with correct use of Optional, early validation, and clear contract documentation.
Audit the following Java class for null safety issues and improve Optional usage.
Class to audit:
[PASTE YOUR CLASS HERE]
Null safety issues to detect and fix:
1. Method parameters not null-checked at method entry:
- If a parameter must not be null, add Objects.requireNonNull(param, "param must not be null") at the start
- Or use @NonNull annotation (Lombok / JSpecify) on the parameter
2. Return values not checked before use:
- result = service.findById(id); result.getName() — NPE if findById returns null
Fix: Methods that may return null should return Optional<T>; callers should use orElseThrow / orElse
3. Optional anti-patterns:
- optional.get() without isPresent() check — throws NoSuchElementException
Fix: Use optional.orElseThrow(() -> new EntityNotFoundException("..."))
- optional.isPresent() followed by optional.get() — verbose, use map/flatMap/orElseGet instead
- Optional used as a method parameter or field type — Optional is designed for return types only
- Optional.of(valueFromDatabase) — use Optional.ofNullable() when the value may be null
4. String operations on potentially null values:
- value.equals("ACTIVE") — NPE if value is null
Fix: "ACTIVE".equals(value) OR Objects.equals(value, "ACTIVE")
5. Collection null returns:
- A method returns null instead of an empty collection when no results are found
Fix: Return Collections.emptyList() or List.of() — never null for collections
6. Ternary null propagation:
- String name = user != null ? user.getProfile() != null ? user.getProfile().getName() : null : null
Fix: Optional.ofNullable(user).map(User::getProfile).map(Profile::getName).orElse("Unknown")
For each finding: the unsafe code, the NPE scenario (which exact dereference throws), and the null-safe replacement.
Prompt 8: Logging Quality Review
When to use: Production incidents are taking too long to diagnose because logs are either missing critical context, too verbose to search through, or logging sensitive data they should not. A systematic logging audit before a release prevents post-incident archaeology.
Review and improve the logging in the following Java class.
Class to review:
[PASTE YOUR CLASS HERE]
Logging issues to find and fix:
1. String concatenation in log statements (evaluated even when log level is disabled):
log.debug("Processing order " + order.getId() + " for customer " + customer.getName());
Fix: log.debug("Processing order {} for customer {}", order.getId(), customer.getName());
2. Missing context in log messages:
log.error("Failed to process order"); — which order? which customer? what failed?
Fix: Include entity IDs, relevant state, and the operation being performed
3. Logging sensitive data (PII / security credentials):
log.info("Authenticating user {} with password {}", username, password);
Fix: Never log passwords, tokens, card numbers, SSNs, or full email addresses
Acceptable: log.info("Authentication attempt for user {}", maskEmail(email));
4. Logging at the wrong level:
- Business-as-usual events at ERROR (user not found, validation failed) → downgrade to WARN
- System-startup events at DEBUG (beans initialised, config loaded) → upgrade to INFO
- Exceptions in catch blocks without the exception object: log.error("Failed") → log.error("Failed", e)
5. Duplicate logging (logging the same event in multiple layers):
- Service logs "Order created" AND repository logs "Order saved" for the same operation
Fix: Log at the highest-level boundary where you have the most context (service layer)
6. Logging inside tight loops:
- log.debug("Processing item {}", item) inside a 10,000-item loop in prod → severe performance impact
Fix: Log before and after the loop with counts; use log.isDebugEnabled() guard for per-item detail
7. Missing correlation ID / request ID in log messages:
Fix: Use MDC (org.slf4j.MDC) to propagate a request ID through all log statements:
MDC.put("requestId", requestId); // set once in a filter, flows to all subsequent log calls
For each finding: the problematic log statement, the issue category, and the corrected version.
Prompt 9: Refactor Long Methods and Reduce Complexity
When to use: A method has grown beyond 40 lines, has a cyclomatic complexity above 10, or has deeply nested conditionals that make it impossible to reason about all the paths without a flowchart. You want a structured refactoring that preserves behaviour while making the code testable and readable.
Refactor the following long, complex Java method to reduce cognitive complexity and improve testability.
Method to refactor (include the full class for context):
[PASTE YOUR CLASS HERE]
Target method name: [METHOD NAME]
Refactoring techniques to apply where appropriate:
1. Extract Method:
- Identify cohesive blocks of 5+ lines with a clear purpose
- Extract each to a private method with a descriptive name
- The extracted method name should read like a sentence: validateOrderItems(), calculateDiscountedTotal()
2. Early Return (Guard Clauses):
- Replace deeply nested if-else with early returns for invalid/edge-case conditions
- Before: if (order != null) { if (order.isValid()) { if (items.size() > 0) { ... } } }
- After: if (order == null) return; if (!order.isValid()) return; if (items.isEmpty()) return;
3. Replace Conditional with Polymorphism:
- If there is a switch/if-else on a type or status enum with 3+ cases, each doing different work
- Introduce a Strategy interface with one implementation per case
4. Replace Magic Numbers with Named Constants:
- All numeric literals used in comparisons or calculations → private static final constants
5. Decompose Conditional:
- Replace complex boolean expressions with a private method that returns a boolean:
- if (order.getStatus() == PENDING && order.getCreatedAt().isBefore(cutoff) && !order.isRetried())
- → if (isOrderEligibleForRetry(order))
Output:
- The refactored class with all changes applied
- A brief explanation of each extraction with before/after complexity metrics (estimated cyclomatic complexity)
- A list of any new private methods created and what they encapsulate
Prompt 10: Full Code Quality Audit Report
When to use: You need a comprehensive review of an entire class before a critical release, a team handoff, or when onboarding new engineers who will own this code. This prompt produces a prioritised, structured report covering all quality dimensions at once.
Perform a comprehensive code quality audit of the following Java class and produce a prioritised improvement report.
Class to audit:
[PASTE YOUR COMPLETE CLASS HERE]
Context:
- This class is used in: [e.g. "a Spring Boot 3.x REST API, singleton service bean, called from 3 controllers"]
- Java version: [e.g. Java 21]
- Known performance constraints: [e.g. "called 500 times/second at peak"]
- Known stability history: [e.g. "NPE in production twice in the last month"]
Audit dimensions (check every one):
[ ] SOLID violations
[ ] Thread-safety (especially if this is a Spring singleton)
[ ] Virtual thread carrier pinning (if Java 21+ codebase)
[ ] N+1 query patterns (if class makes DB calls)
[ ] Exception handling anti-patterns
[ ] Resource leaks
[ ] Null safety and Optional misuse
[ ] Logging quality (missing context, wrong level, sensitive data)
[ ] Long methods / high cyclomatic complexity
[ ] Magic numbers and poorly named variables
[ ] Unnecessary comments (comments that restate the code rather than explain the WHY)
[ ] Missing input validation on public methods
[ ] Transactional boundary correctness (if @Transactional is used)
Output format:
For each finding:
1. Issue category: [e.g. "Resource Leak"]
2. Severity: CRITICAL (data loss / security) / HIGH (production failure) / MEDIUM (reliability) / LOW (maintainability)
3. Location: method name + approximate line
4. Description: one sentence
5. Fix: concrete before/after code
Summary at the end:
- Total findings by severity
- Top 3 highest-priority fixes
- Estimated refactoring effort (hours)
Workflow: How to Use These Prompts in Code Review
For pull request reviews, run Prompt 5 (thread-safety) and Prompt 6 (resource leaks) on every new service or utility class — these categories consistently produce production incidents when missed. Run Prompt 4 (exception handling) on any class that introduces new try-catch blocks. For legacy code coming under new ownership or about to receive significant changes, run Prompt 10 (full audit) to establish a baseline before touching anything.
Frequently Asked Questions (FAQs)
Q1: How do I handle a class that is too long to paste into a single prompt?
Split by responsibility first. If a class has three distinct concerns (validation, persistence, notification), paste each logical block separately with a note: “This is part 1 of 3 of class OrderService — focus only on the validation methods.” Run Prompt 1 (SOLID) on the full class to confirm it should be split, then run the specialised prompts on each extracted section.
Q2: The AI flagged my synchronized block as a carrier pinning risk — but I am on Java 11. Does it matter?
On Java 11, carrier pinning does not exist because virtual threads do not exist. However, rewriting synchronized blocks to use ReentrantLock is still worthwhile for two reasons: it future-proofs the code for a Java 21+ migration, and ReentrantLock supports timed lock attempts and interruptible locking that synchronized does not. The refactoring is low-risk and purely additive in value.
Q3: Should I run these prompts on auto-generated code (MapStruct mappers, JPA metamodel)?
No — auto-generated code should not be manually reviewed or modified. Run these prompts only on handwritten classes. If Prompt 3 (N+1) finds issues that trace back to generated repository methods, the fix goes into the repository interface (adding @EntityGraph) or the service layer (changing the query), not the generated code itself.
Q4: Can these prompts replace a static analysis tool like SonarQube?
They complement each other rather than substitute. SonarQube runs on every commit, is fast, and has precise rule coverage for well-defined patterns. AI code review excels at reasoning about intent, cross-class interactions, and patterns that are difficult to express as fixed rules — such as whether a synchronized block wraps a blocking call given the specific dependencies injected. Use both: SonarQube as the continuous gate, AI prompts for deeper analysis on critical paths and during major changes.
See Also
- 10 AI Prompts for Spring Boot Development
- Virtual Threads vs Platform Threads — Benchmarks and Code
- 10 AI Prompts to Generate JUnit 6 Tests for New Projects
- AI Prompts Playbook: Upgrading Java 8 → 11 → 17 → 21 → 25
- Java Concurrency Deep Dive: CompletableFuture, ExecutorService, ForkJoinPool
Conclusion
These 10 prompts cover the full spectrum of Java code quality concerns: SOLID design, virtual thread safety, N+1 queries, exception handling, concurrency bugs, resource leaks, null safety, logging quality, complexity reduction, and comprehensive audits. Use them systematically — one targeted prompt per concern per class — rather than running the full audit prompt on every file. The combination of focused AI analysis and your own domain knowledge catches problems that neither approach finds alone.