Spring Boot Skills
GitHub
AI & MCP

Spring AI Integration

Use when integrating LLMs, chat clients, embeddings, RAG pipelines, or AI agents into Spring Boot. Covers Spring AI ChatClient, prompt templates, embeddings, vector stores, and structured output. Use when user mentions Spring AI, LLM, ChatGPT, Claude, RAG, embeddings.

View SKILL.md

Dependencies

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>1.0.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <!-- Choose your model provider — 1.0 GA renamed every starter to spring-ai-starter-* -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-model-anthropic</artifactId>
    </dependency>
    <!-- OR -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-model-openai</artifactId>
    </dependency>

    <!-- For RAG / vector search -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-vector-store-pgvector</artifactId>
    </dependency>
</dependencies>

Watch the artifact names. 1.0 GA dropped the old spring-ai-<x>-spring-boot-starter coordinates. The pattern is now spring-ai-starter-model-<provider> (e.g. -model-anthropic, -model-openai) and spring-ai-starter-vector-store-<store>. Agents trained on pre-GA Spring AI will emit the dead names — they resolve to nothing in Maven Central.

ChatClient — Basic Usage

@Service
@RequiredArgsConstructor
public class DocumentSummaryService {

    private final ChatClient chatClient;

    public String summarize(String conversationId, String content) {
        return chatClient.prompt()
            .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
            .user(u -> u.text("Summarize the following document in 3 bullet points:\n\n{content}")
                .param("content", content))
            .call()
            .content();
    }

    // With system prompt
    public String analyzeFinancial(String conversationId, String document, String language) {
        return chatClient.prompt()
            .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
            .system("You are a financial analyst. Respond in {language}.")
            .system(s -> s.param("language", language))
            .user(document)
            .call()
            .content();
    }
}

Every call using the configured memory advisor must provide a user- or session-scoped ChatMemory.CONVERSATION_ID. Never use one shared conversation ID for all users.

ChatClient Bean Configuration

@Configuration
public class AiConfig {

    @Bean
    public ChatMemory chatMemory() {
        // 1.0 GA: InMemoryChatMemory is GONE. Use MessageWindowChatMemory —
        // it caps history to a sliding window and defaults to an in-memory repository.
        return MessageWindowChatMemory.builder()
            .maxMessages(20)
            .build();
    }

    @Bean
    public ChatClient chatClient(ChatClient.Builder builder, ChatMemory chatMemory) {
        return builder
            .defaultSystem("You are a helpful assistant for an e-commerce platform.")
            .defaultAdvisors(
                MessageChatMemoryAdvisor.builder(chatMemory).build(), // GA: builder, not new(...)
                new SimpleLoggerAdvisor() // logs prompts/responses
            )
            .build();
    }
}

Prompt Templates (externalized)

// src/main/resources/prompts/analyze-order.st
// Analyze this order and identify any anomalies:
// Customer: {customerEmail}
// Items: {items}
// Total: {total}
// Flag any unusual patterns.

@Service
@RequiredArgsConstructor
public class OrderAnalysisService {

    private final ChatClient chatClient;

    @Value("classpath:prompts/analyze-order.st")
    private Resource promptTemplate;

    public String analyzeOrder(String conversationId, Order order) {
        return chatClient.prompt()
            .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
            .user(u -> u.text(promptTemplate)
                .param("customerEmail", order.getCustomerEmail()) // must match {customerEmail} in the .st
                .param("items", order.getItems().toString())
                .param("total", order.getTotal()))
            .call()
            .content();
    }
}

Structured Output

// Define the target record
public record OrderClassification(
    String category,
    String priority,
    List<String> tags,
    boolean requiresManualReview
) {}

@Service
public class OrderClassifier {

    public OrderClassification classify(String orderDescription) {
        return chatClient.prompt()
            .user("Classify this order: " + orderDescription)
            .call()
            .entity(OrderClassification.class); // Spring AI handles JSON parsing
    }
}

RAG Pipeline

@Configuration
public class RagConfig {

    // No manual VectorStore bean — the spring-ai-starter-vector-store-pgvector
    // starter auto-configures one. Just inject it. (The old `new PgVectorStore(...)`
    // constructor is removed in GA; if you must build one, use PgVectorStore.builder(...).)

    @Bean
    public ChatClient ragChatClient(ChatClient.Builder builder, VectorStore vectorStore) {
        return builder
            .defaultAdvisors(
                QuestionAnswerAdvisor.builder(vectorStore)
                    .searchRequest(SearchRequest.builder().topK(5).build()) // GA: builder, not defaults().withTopK()
                    .build()
            )
            .build();
    }
}

@Service
@RequiredArgsConstructor
public class KnowledgeService {

    private final VectorStore vectorStore;
    private final ChatClient ragChatClient;

    // Ingest documents
    public void ingest(List<String> documents) {
        List<Document> docs = documents.stream()
            .map(content -> new Document(content))
            .toList();
        vectorStore.add(docs);
    }

    // Query with RAG
    public String ask(String question) {
        return ragChatClient.prompt()
            .user(question)
            .call()
            .content();
    }
}

Streaming Responses

@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> stream(@RequestParam String prompt) {
    return chatClient.prompt()
        .user(prompt)
        .stream()
        .content();
}

application.yml

spring:
  ai:
    anthropic:
      api-key: ${ANTHROPIC_API_KEY}
      chat:
        options:
          model: ${ANTHROPIC_MODEL}
          max-tokens: 2048
          temperature: 0.7
    # OR for OpenAI:
    openai:
      api-key: ${OPENAI_API_KEY}
      chat:
        options:
          model: ${OPENAI_MODEL}
    vectorstore:
      pgvector:
        initialize-schema: true
        dimensions: 1536

Gotchas

  • Agent binds .param("customer", ...) when the .st file says {customerEmail} — the default renderer fails with "Not all variables were replaced"; keep param names and template placeholders identical
  • Agent uses pre-GA artifact names (spring-ai-anthropic-spring-boot-starter) — GA is spring-ai-starter-model-anthropic
  • Agent writes new MessageChatMemoryAdvisor(new InMemoryChatMemory()) — both removed in GA; use MessageChatMemoryAdvisor.builder(chatMemory) + MessageWindowChatMemory
  • Agent writes SearchRequest.defaults().withTopK(n) — GA is SearchRequest.builder().topK(n).build()
  • Agent hardcodes API keys — always use environment variables / ${...}
  • Agent hardcodes provider model IDs - configure them externally because model catalogs change
  • Agent builds prompts with string concatenation — use .param() template variables
  • Agent puts prompts inline in code — externalize to src/main/resources/prompts/
  • Agent ignores structured output — use .entity(MyClass.class) instead of parsing manually
  • Agent uses .entity(List.class) for a list — generics erase; pass new ParameterizedTypeReference<List<X>>() {}
  • Agent skips error handling for API calls — wrap in try/catch, handle NonTransientAiException (don't retry) vs TransientAiException (retry)
  • Agent forgets a per-user conversationId on the memory advisor — all users share one chat history
  • Agent uses wrong model string — verify model names against provider docs
FROM THE REPOSITORY

Examples & templates

bad-ai-service.javaexamples
Source on GitHub
// ❌ BAD — hardcoded key, string concatenation, inline prompts, no error handling

@Service
public class OrderAnalysisService {

    // ❌ Hardcoded API key in source code — should be in environment variable
    private static final String API_KEY = "sk-ant-abc123...";

    @Autowired
    private ChatClient.Builder chatClientBuilder;         // builds new client per call — wasteful

    public String analyzeOrder(Order order) {
        ChatClient client = chatClientBuilder.build();    // should be injected as singleton

        // ❌ String concatenation for prompt — use .param() with templates
        String prompt = "Analyze this order: " +
            "Customer: " + order.getCustomerEmail() + ", " +
            "Items: " + order.getItems() + ", " +
            "Total: " + order.getTotal();

        // ❌ Inline prompt with no externalization
        String response = client.prompt()
            .user(prompt)                                 // raw concatenated string
            .call()
            .content();                                   // returns raw String, not structured

        // ❌ Manual JSON parsing instead of .entity()
        try {
            ObjectMapper mapper = new ObjectMapper();     // should be injected, not created
            return mapper.readTree(response)
                .get("analysis").asText();
        } catch (Exception e) {
            return "Error";                               // swallows exception, no logging
        }
    }

    // ❌ No Flux/streaming for long responses — blocks thread
    // ❌ No observability — can't track token usage or latency
    // ❌ No fallback behavior when AI service is unavailable
}
good-ai-service.javaexamples
Source on GitHub
// ✅ GOOD — injected ChatClient, externalized prompts, structured output, error handling

@Service
@Slf4j
public class OrderAnalysisService {

    // One-shot analysis: a stateless client built from the auto-configured (prototype)
    // ChatClient.Builder. The shared ChatClient bean carries MessageChatMemoryAdvisor,
    // which needs a per-user conversation id — without one, Spring AI 2 rejects the
    // call and 1.x silently pools every user into one shared chat history.
    private final ChatClient chatClient;

    public OrderAnalysisService(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    // Externalized prompt template loaded from classpath:prompts/analyze-order.st
    @Value("classpath:prompts/analyze-order.st")
    private Resource analyzePrompt;

    public OrderAnalysis analyzeOrder(Order order) {
        try {
            return chatClient.prompt()
                .user(u -> u
                    .text(analyzePrompt)
                    .param("customerEmail", order.getCustomerEmail())
                    .param("items", order.getItems().toString())
                    .param("total", order.getTotal().toString()))
                .call()
                .entity(OrderAnalysis.class);  // structured output — auto-parsed to record
        } catch (Exception e) {
            log.error("AI analysis failed for order {}", order.getId(), e);
            return OrderAnalysis.fallback("Analysis unavailable");
        }
    }

    // Streaming for long responses
    public Flux<String> streamRecommendations(UUID customerId, List<Order> history) {
        return chatClient.prompt()
            .system("You are a product recommendation engine.")
            .user(u -> u
                .text("Based on order history: {history}, suggest products.")
                .param("history", history.toString()))
            .stream()
            .content();
    }

    public record OrderAnalysis(
        String riskLevel,
        String summary,
        List<String> recommendations
    ) {
        public static OrderAnalysis fallback(String reason) {
            return new OrderAnalysis("UNKNOWN", reason, List.of());
        }
    }
}
AiServiceTemplate.javatemplates
Source on GitHub
package com.example.ai.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;

import java.util.List;

@Slf4j
@Service
public class AiServiceTemplate {

    // One-shot analysis: a stateless client built from the auto-configured (prototype)
    // ChatClient.Builder. The shared ChatClient bean carries MessageChatMemoryAdvisor,
    // which needs a per-user conversation id — without one, Spring AI 2 rejects the
    // call and 1.x silently pools every user into one shared chat history.
    private final ChatClient chatClient;

    public AiServiceTemplate(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    @Value("classpath:prompts/analyze-order.st")
    private Resource analyzePrompt;

    /**
     * Simple prompt with structured output — auto-parsed to Java record.
     */
    public OrderAnalysis analyzeOrder(String customerEmail, String items, String total) {
        try {
            return chatClient.prompt()
                .user(u -> u
                    .text(analyzePrompt)
                    .param("customerEmail", customerEmail)
                    .param("items", items)
                    .param("total", total))
                .call()
                .entity(OrderAnalysis.class);
        } catch (Exception e) {
            log.error("AI analysis failed", e);
            return OrderAnalysis.fallback("Service unavailable");
        }
    }

    /**
     * Streaming response for long-form content.
     */
    public Flux<String> streamAnalysis(String context) {
        return chatClient.prompt()
            .system("You are a business analyst specializing in e-commerce orders.")
            .user(context)
            .stream()
            .content();
    }

    /**
     * Structured list output — Spring AI parses JSON array into List.
     */
    public List<ProductRecommendation> getRecommendations(String orderHistory) {
        try {
            return chatClient.prompt()
                .system("Recommend products based on order history. Return as JSON array.")
                .user(orderHistory)
                .call()
                .entity(new org.springframework.core.ParameterizedTypeReference<>() {});
        } catch (Exception e) {
            log.error("Recommendation generation failed", e);
            return List.of();
        }
    }

    public record OrderAnalysis(String riskLevel, String summary, List<String> recommendations) {
        public static OrderAnalysis fallback(String reason) {
            return new OrderAnalysis("UNKNOWN", reason, List.of());
        }
    }

    public record ProductRecommendation(String productName, String reason, double confidence) {}
}
analyze-order.sttemplates
Source on GitHub
You are an e-commerce order analyst. Analyze the following order for risk and provide recommendations.

Customer: {customerEmail}
Items: {items}
Total: {total}

Respond with a JSON object containing:
- riskLevel: one of LOW, MEDIUM, HIGH
- summary: a one-sentence analysis of the order
- recommendations: an array of actionable suggestions (max 3)

Focus on:
1. Unusual order patterns (high value, bulk quantities)
2. Customer behavior signals
3. Inventory or fulfillment considerations

Verification scope

Compiled sources
No
Runtime checks
No provider or conversation tests

Coverage describes checked-in fixtures, not a guarantee for every snippet or project.

Coverage matrix & agent evidence
ADD TO YOUR AGENT

Install Boot 3 skills

claude plugin marketplace add rrezartprebreza/spring-boot-skills
claude plugin install spring-boot-3-skills@spring-boot-skills