Java SpringAI

Prompt Engineering for Java Developers — Techniques with Spring AI Examples

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.0 for code generation and factual questions; use higher values only for creative tasks
Topics: Java SpringAI
← Newer Post Older Post →