Prompt Engineering for Java Developers — Techniques with Spring AI Examples
Prompt engineering is the practice of crafting inputs to language models to reliably get high-quality, accurate outputs. The same model with a poorly written prompt returns vague answers; a well-engineered prompt returns structured, actionable, correct results. This tutorial covers the essential techniques every Java developer needs when building AI applications with Spring AI.
Technique 1: Be Specific and Provide Context
// Bad prompt — vague, no context
"Explain transactions"
// Good prompt — specific, scoped, audience-aware
"""
Explain @Transactional in Spring Boot for a Java developer who knows JPA
but is new to transaction management. Include:
- What it does under the hood (Spring AOP proxy)
- Propagation types with one practical example each
- When NOT to use it (same-class method calls)
- One common mistake and how to fix it
"""
@Service
public class PromptQualityService {
private final ChatClient chatClient;
public PromptQualityService(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
public String explain(String topic, String audience, List<String> requirements) {
String requirementList = requirements.stream()
.map(r -> "- " + r)
.collect(Collectors.joining("\n"));
return chatClient.prompt()
.user("""
Explain %s for %s.
Cover these specific points:
%s
Keep each point to 2-3 sentences with a code example.
""".formatted(topic, audience, requirementList))
.call()
.content();
}
}
Technique 2: Role Prompting
// Assign a role to get domain-expert quality responses
@Service
public class RolePromptService {
private final ChatClient securityReviewer;
private final ChatClient performanceExpert;
private final ChatClient codeReviewer;
public RolePromptService(ChatClient.Builder builder) {
this.securityReviewer = builder
.defaultSystem("""
You are a senior application security engineer specializing in
Java and Spring Boot. You identify OWASP Top 10 vulnerabilities,
insecure coding patterns, and always suggest specific fixes.
""")
.build();
this.performanceExpert = builder
.defaultSystem("""
You are a Java performance engineer. You identify bottlenecks,
N+1 query problems, memory leaks, and thread contention issues.
You always profile before optimizing and cite JVM internals.
""")
.build();
this.codeReviewer = builder
.defaultSystem("""
You are a principal Java engineer doing code review. You check
for correctness, SOLID principles, clean code, and test coverage.
Be specific, cite line numbers when shown code, and explain why.
""")
.build();
}
public String reviewSecurity(String code) { return securityReviewer.prompt().user(code).call().content(); }
public String reviewPerformance(String code) { return performanceExpert.prompt().user(code).call().content(); }
public String reviewCode(String code) { return codeReviewer.prompt().user(code).call().content(); }
}
Technique 3: Few-Shot Prompting
Provide input-output examples to teach the model exactly what format you want:
public String classifyIssue(String issueTitle) {
return chatClient.prompt()
.user("""
Classify the following GitHub issue into one category:
[BUG, FEATURE, DOCS, PERFORMANCE, SECURITY, QUESTION]
Examples:
Issue: "NullPointerException when calling /api/users with empty body"
Category: BUG
Issue: "Add support for MongoDB as a vector store"
Category: FEATURE
Issue: "Update README with Docker Compose instructions"
Category: DOCS
Issue: "Queries take 30s on 1M records table"
Category: PERFORMANCE
Now classify:
Issue: "%s"
Category:
""".formatted(issueTitle))
.call()
.content()
.trim();
}
Output
classifyIssue("SQL injection possible in user search endpoint")
→ SECURITY
classifyIssue("Add streaming support to ChatClient")
→ FEATURE
classifyIssue("Application crashes when JWT token is expired")
→ BUG
Technique 4: Chain-of-Thought Prompting
Ask the model to reason step by step before giving the final answer — dramatically improves accuracy for complex tasks:
public String analyzeComplexity(String code) {
return chatClient.prompt()
.user("""
Analyze the time complexity of this Java code.
Step 1: Identify all loops and their bounds
Step 2: Identify nested loops and multiply their bounds
Step 3: Identify recursive calls and their recurrence
Step 4: State the overall Big-O complexity
Step 5: Suggest a more efficient algorithm if possible
Code:
```java
%s
```
""".formatted(code))
.call()
.content();
}
Technique 5: Output Format Constraints
public String generateChangeLog(String diffText) {
return chatClient.prompt()
.user("""
Generate a changelog entry from this git diff.
Rules:
- Start with one of: Added / Changed / Fixed / Removed / Security
- Maximum 80 characters per line
- Use present tense ("Add feature" not "Added feature")
- No technical jargon — write for end users
- Output ONLY the changelog lines, no explanation
Git diff:
%s
""".formatted(diffText))
.call()
.content();
}
Technique 6: Negative Constraints
public String generateDocumentation(String code) {
return chatClient.prompt()
.user("""
Write Javadoc for this method.
DO NOT:
- Repeat what the code already says (e.g. "this method returns...")
- Use words like "simply", "just", "obviously"
- Describe implementation details
- Use first person
DO:
- Explain WHY, not WHAT
- Document edge cases and null handling
- Note thread safety if relevant
- Link related methods with @see
Method:
```java
%s
```
""".formatted(code))
.call()
.content();
}
Technique 7: Temperature Control per Use Case
// Low temperature (0.0–0.3) — factual, deterministic, code generation
public String generateCode(String requirement) {
return chatClient.prompt()
.user(requirement)
.options(OpenAiChatOptions.builder().temperature(0.1).build())
.call()
.content();
}
// Medium temperature (0.5–0.7) — balanced, explanations
public String explain(String concept) {
return chatClient.prompt()
.user("Explain " + concept)
.options(OpenAiChatOptions.builder().temperature(0.5).build())
.call()
.content();
}
// High temperature (0.8–1.0) — creative, brainstorming
public String brainstorm(String topic) {
return chatClient.prompt()
.user("Brainstorm 10 creative uses of " + topic + " in Java applications")
.options(OpenAiChatOptions.builder().temperature(0.9).build())
.call()
.content();
}
Key Points
- Specificity is the single biggest lever — vague prompts get vague answers
- Role prompting works because LLMs were trained on text written by domain experts — activating the role primes relevant knowledge
- Few-shot examples are more reliable than descriptions when you need exact output format
- Chain-of-thought forces the model to not skip steps — adds latency but prevents incorrect shortcuts
- Set
temperature=0.0for code generation and factual questions; use higher values only for creative tasks
Comments