diff --git a/spring-ai-modules/pom.xml b/spring-ai-modules/pom.xml
index 94a9814b6426..8eef8c357c87 100644
--- a/spring-ai-modules/pom.xml
+++ b/spring-ai-modules/pom.xml
@@ -39,5 +39,6 @@
spring-ai-vector-stores
spring-ai-mcp-annotations
spring-ai-subagent-orchestrator
+ spring-ai-anthropic
diff --git a/spring-ai-modules/spring-ai-anthropic/pom.xml b/spring-ai-modules/spring-ai-anthropic/pom.xml
new file mode 100644
index 000000000000..3492bccc2059
--- /dev/null
+++ b/spring-ai-modules/spring-ai-anthropic/pom.xml
@@ -0,0 +1,59 @@
+
+
+ 4.0.0
+
+
+ com.baeldung
+ spring-ai-modules
+ 0.0.1
+ ../pom.xml
+
+
+ spring-ai-anthropic
+ spring-ai-anthropic
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.ai
+ spring-ai-starter-model-anthropic
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+ org.springframework.ai
+ spring-ai-bom
+ ${spring-ai.version}
+ pom
+ import
+
+
+
+
+
+ 21
+ 2.0.1
+ 3.5.13
+ 1.5.18
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/spring-ai-modules/spring-ai-anthropic/src/main/java/com/baeldung/springai/anthropic/Application.java b/spring-ai-modules/spring-ai-anthropic/src/main/java/com/baeldung/springai/anthropic/Application.java
new file mode 100644
index 000000000000..c35f74b87bff
--- /dev/null
+++ b/spring-ai-modules/spring-ai-anthropic/src/main/java/com/baeldung/springai/anthropic/Application.java
@@ -0,0 +1,12 @@
+package com.baeldung.springai.anthropic;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class Application {
+
+ public static void main(String[] args) {
+ SpringApplication.run(Application.class, args);
+ }
+}
diff --git a/spring-ai-modules/spring-ai-anthropic/src/main/java/com/baeldung/springai/anthropic/ChatResponseWithMetadataDto.java b/spring-ai-modules/spring-ai-anthropic/src/main/java/com/baeldung/springai/anthropic/ChatResponseWithMetadataDto.java
new file mode 100644
index 000000000000..0a5b42fcf7c4
--- /dev/null
+++ b/spring-ai-modules/spring-ai-anthropic/src/main/java/com/baeldung/springai/anthropic/ChatResponseWithMetadataDto.java
@@ -0,0 +1,34 @@
+package com.baeldung.springai.anthropic;
+
+import org.springframework.ai.chat.model.ChatResponse;
+
+public record ChatResponseWithMetadataDto(String responseText,
+ Integer promptTokens,
+ Integer completionTokens,
+ Long cacheReadInputTokens,
+ Long cacheWriteInputTokens) {
+
+ public static ChatResponseWithMetadataDto fromChatResponse(ChatResponse chatResponse) {
+ if (chatResponse == null || chatResponse.getResult() == null) {
+ throw new RuntimeException("Client response has no results");
+ }
+
+ String responseText = chatResponse.getResult()
+ .getOutput()
+ .getText();
+ Integer promptTokens = chatResponse.getMetadata()
+ .getUsage()
+ .getPromptTokens();
+ Integer completionTokens = chatResponse.getMetadata()
+ .getUsage()
+ .getCompletionTokens();
+ Long cacheReadInputTokens = chatResponse.getMetadata()
+ .getUsage()
+ .getCacheReadInputTokens();
+ Long cacheWriteInputTokens = chatResponse.getMetadata()
+ .getUsage()
+ .getCacheWriteInputTokens();
+
+ return new ChatResponseWithMetadataDto(responseText, promptTokens, completionTokens, cacheReadInputTokens, cacheWriteInputTokens);
+ }
+}
diff --git a/spring-ai-modules/spring-ai-anthropic/src/main/java/com/baeldung/springai/anthropic/ChatWithPromptCachingService.java b/spring-ai-modules/spring-ai-anthropic/src/main/java/com/baeldung/springai/anthropic/ChatWithPromptCachingService.java
new file mode 100644
index 000000000000..f496e6de9561
--- /dev/null
+++ b/spring-ai-modules/spring-ai-anthropic/src/main/java/com/baeldung/springai/anthropic/ChatWithPromptCachingService.java
@@ -0,0 +1,41 @@
+package com.baeldung.springai.anthropic;
+
+import java.util.UUID;
+
+import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.stereotype.Service;
+
+@Service
+public class ChatWithPromptCachingService {
+
+ private final ChatClient chatClient;
+
+ public ChatWithPromptCachingService(ChatClient.Builder builder) {
+ this.chatClient = builder.build();
+ }
+
+ public ChatResponseWithMetadataDto chat(String userMessage) {
+ return chat(userMessage, PromptsUtils.LONG_SYSTEM_PROMPT);
+ }
+
+ public ChatResponseWithMetadataDto chat(String userMessage, UUID uniquePromptId) {
+ return chat(userMessage, PromptsUtils.LONG_SYSTEM_PROMPT + "\nTEST_ID=" + uniquePromptId);
+ }
+
+ public ChatResponseWithMetadataDto chat(String userMessage, String systemPrompt) {
+ ChatResponse response = chatClient
+ .prompt()
+ .system(systemPrompt)
+ .user(userMessage)
+ .call()
+ .chatClientResponse()
+ .chatResponse();
+
+ if (response == null || response.getResult() == null) {
+ throw new RuntimeException("Client response has no results");
+ }
+
+ return ChatResponseWithMetadataDto.fromChatResponse(response);
+ }
+}
diff --git a/spring-ai-modules/spring-ai-anthropic/src/main/java/com/baeldung/springai/anthropic/PromptsUtils.java b/spring-ai-modules/spring-ai-anthropic/src/main/java/com/baeldung/springai/anthropic/PromptsUtils.java
new file mode 100644
index 000000000000..9391320e812b
--- /dev/null
+++ b/spring-ai-modules/spring-ai-anthropic/src/main/java/com/baeldung/springai/anthropic/PromptsUtils.java
@@ -0,0 +1,59 @@
+package com.baeldung.springai.anthropic;
+
+public final class PromptsUtils {
+
+ private PromptsUtils() {
+ }
+
+ public static final String LONG_SYSTEM_PROMPT = """
+ You are a senior software engineering assistant specializing in Java, Spring Boot, Spring AI, distributed systems, APIs, databases, observability, testing, and production troubleshooting.
+
+ Your primary responsibility is to provide accurate, practical, production-oriented answers. When answering programming questions, first understand the user's actual goal and constraints. Prefer simple solutions when they are sufficient, but explain important trade-offs when a decision affects maintainability, performance, reliability, security, or operational complexity.
+
+ When writing Java code, prefer modern Java conventions and clear naming. Favor immutable data where practical. Use dependency injection rather than manually constructing application dependencies. Keep classes focused on one responsibility. Avoid unnecessary abstractions, excessive interfaces, and patterns that do not provide a concrete benefit.
+
+ When answering Spring Boot questions, assume the application uses conventional Spring Boot configuration unless the user explicitly states otherwise. Prefer configuration through application.yml or application.properties when appropriate. Explain which configuration belongs to Spring Boot, which belongs to a third-party library, and which properties are custom application properties.
+
+ When answering Spring AI questions, distinguish between Spring AI behavior and the behavior of the underlying model provider. If a feature depends on Anthropic, OpenAI, or another provider, make that distinction explicit. Do not assume that a Spring AI option can override a provider-side limitation. When discussing token usage, distinguish input tokens, output tokens, cached input tokens, cache creation tokens, and cache read tokens.
+
+ When debugging an issue, identify the most likely cause first. Then provide a concrete way to verify the hypothesis. Prefer observable evidence such as logs, response metadata, HTTP requests, metrics, or configuration values rather than assumptions.
+
+ For API integrations, pay attention to request structure, authentication, headers, model names, provider-specific limitations, rate limits, token limits, and response metadata. If a behavior is controlled by the remote API rather than the client library, say so explicitly.
+
+ For caching systems, explain the difference between enabling a cache feature and actually obtaining a cache hit. A cache may be configured correctly while still producing zero cache reads because the request does not satisfy the provider's requirements. Consider minimum cacheable size, cache boundaries, exact prefix matching, request changes, expiration, and provider-specific rules.
+
+ When suggesting configuration, provide a complete example when possible. Clearly identify which lines are required and which are optional. Do not invent configuration properties. If you are uncertain whether a property exists in a particular library version, state that and recommend checking the version-specific documentation.
+
+ When discussing performance, avoid making claims without explaining what is being measured. Distinguish latency, throughput, token consumption, model processing time, network time, and application-side processing.
+
+ When discussing security, never recommend disabling TLS certificate validation, hostname verification, authentication, authorization, or other security controls in production. If a development-only workaround is necessary, clearly label it as development-only and explain the safer production alternative.
+
+ When providing database advice, consider transaction boundaries, indexes, connection pools, isolation levels, locking, pagination, query performance, and consistency requirements. Do not recommend changing database settings without explaining the relevant trade-offs.
+
+ When providing concurrency advice, consider thread safety, synchronization, locks, executors, virtual threads, asynchronous processing, race conditions, and resource limits. Avoid claiming that asynchronous code is automatically faster.
+
+ When providing testing advice, distinguish unit tests, integration tests, contract tests, end-to-end tests, and performance tests. Prefer tests that reproduce the actual failure mode. When useful, show a minimal reproducible test case.
+
+ When the user provides code, analyze the code they actually provided before suggesting a completely different architecture. Point out the smallest change that can solve the problem, then optionally mention a cleaner or more scalable alternative.
+
+ When the user asks a question that can be answered directly, answer directly before providing background information. Avoid unnecessary introductions. Use code blocks for code and concise bullet points for multiple independent recommendations.
+
+ If the user's question depends on a version-specific behavior, determine the relevant library or framework version before making a definitive statement. Different versions may expose different configuration properties, APIs, or defaults.
+
+ Do not fabricate logs, API responses, configuration properties, library methods, or documentation. If an exact value is unknown, say that it needs to be verified rather than presenting an invented value as fact.
+
+ For troubleshooting, structure the answer around:
+
+ What is happening.
+ Why it is happening.
+ How to verify it.
+ The smallest fix.
+ Any important caveats.
+
+ Maintain a professional but conversational tone. Assume the user is technically capable and wants useful implementation details rather than generic explanations.
+
+ The system prompt itself should remain stable across requests whenever possible. Dynamic information such as the current user question, conversation-specific instructions, timestamps, or temporary context should be placed after the stable system content rather than changing the cached prefix.
+
+ Your answers should prioritize correctness, practical implementation, and clear reasoning. If there are multiple valid approaches, identify the recommended approach first and briefly explain when the alternatives make sense.
+ """;
+}
diff --git a/spring-ai-modules/spring-ai-anthropic/src/main/resources/application.yml b/spring-ai-modules/spring-ai-anthropic/src/main/resources/application.yml
new file mode 100644
index 000000000000..b69e42fb30a2
--- /dev/null
+++ b/spring-ai-modules/spring-ai-anthropic/src/main/resources/application.yml
@@ -0,0 +1,10 @@
+spring:
+ ai:
+ anthropic:
+ api-key: ${ANTHROPIC_API_KEY}
+ chat:
+ options:
+ model: claude-sonnet-4-6
+ max-tokens: 500
+ cache-options:
+ strategy: SYSTEM_ONLY
diff --git a/spring-ai-modules/spring-ai-anthropic/src/test/java/com/baeldung/springai/anthropic/ApplicationTest.java b/spring-ai-modules/spring-ai-anthropic/src/test/java/com/baeldung/springai/anthropic/ApplicationTest.java
new file mode 100644
index 000000000000..d48e7bb6fd69
--- /dev/null
+++ b/spring-ai-modules/spring-ai-anthropic/src/test/java/com/baeldung/springai/anthropic/ApplicationTest.java
@@ -0,0 +1,10 @@
+package com.baeldung.springai.anthropic;
+
+import org.junit.jupiter.api.Test;
+
+class ApplicationTest {
+
+ @Test
+ void contextLoads() {
+ }
+}
diff --git a/spring-ai-modules/spring-ai-anthropic/src/test/java/com/baeldung/springai/anthropic/ChatWithPromptCachingDisabledIntegrationTest.java b/spring-ai-modules/spring-ai-anthropic/src/test/java/com/baeldung/springai/anthropic/ChatWithPromptCachingDisabledIntegrationTest.java
new file mode 100644
index 000000000000..50b8de48247e
--- /dev/null
+++ b/spring-ai-modules/spring-ai-anthropic/src/test/java/com/baeldung/springai/anthropic/ChatWithPromptCachingDisabledIntegrationTest.java
@@ -0,0 +1,37 @@
+package com.baeldung.springai.anthropic;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest(properties = { "spring.ai.anthropic.chat.cache-options.strategy=NONE" })
+class ChatWithPromptCachingDisabledIntegrationTest {
+
+ @Autowired
+ private ChatWithPromptCachingService service;
+
+ @Test
+ void chat_whenPromptCachingDisabled_returnsResponse() {
+ String chatMessage = "hello there";
+
+ ChatResponseWithMetadataDto response = service.chat(chatMessage);
+
+ System.out.println("Response1: " + response);
+ System.out.println("Response1 promptTokens: " + response.promptTokens());
+ System.out.println("Response1 completionTokens: " + response.completionTokens());
+ assertThat(response).isNotNull();
+ assertThat(response.promptTokens()).isGreaterThan(1030);
+ assertThat(response.cacheReadInputTokens()).isEqualTo(0);
+
+ response = service.chat(chatMessage + " again");
+
+ System.out.println("Response2: " + response);
+ System.out.println("Response2 promptTokens: " + response.promptTokens());
+ System.out.println("Response2 completionTokens: " + response.completionTokens());
+ assertThat(response).isNotNull();
+ assertThat(response.promptTokens()).isGreaterThan(1030);
+ assertThat(response.cacheReadInputTokens()).isEqualTo(0);
+ }
+}
diff --git a/spring-ai-modules/spring-ai-anthropic/src/test/java/com/baeldung/springai/anthropic/ChatWithPromptCachingEnabledIntegrationTest.java b/spring-ai-modules/spring-ai-anthropic/src/test/java/com/baeldung/springai/anthropic/ChatWithPromptCachingEnabledIntegrationTest.java
new file mode 100644
index 000000000000..2dbf51827f36
--- /dev/null
+++ b/spring-ai-modules/spring-ai-anthropic/src/test/java/com/baeldung/springai/anthropic/ChatWithPromptCachingEnabledIntegrationTest.java
@@ -0,0 +1,43 @@
+package com.baeldung.springai.anthropic;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.UUID;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class ChatWithPromptCachingEnabledIntegrationTest {
+
+ @Autowired
+ private ChatWithPromptCachingService service;
+ private static final UUID TEST_ID = UUID.randomUUID();
+
+ @Test
+ void chat_whenPromptCachingEnabled_returnsResponse() {
+ String chatMessage = "hello there";
+
+ ChatResponseWithMetadataDto response = service.chat(chatMessage, TEST_ID);
+
+ System.out.println("Response1: " + response);
+ System.out.println("Response1 promptTokens: " + response.promptTokens());
+ System.out.println("Response1 completionTokens: " + response.completionTokens());
+ assertThat(response).isNotNull();
+ assertThat(response.promptTokens()).isLessThan(50);
+ assertThat(response.cacheWriteInputTokens()).isGreaterThan(1000);
+ assertThat(response.cacheReadInputTokens()).isEqualTo(0);
+
+
+ response = service.chat(chatMessage + " again", TEST_ID);
+
+ System.out.println("Response2: " + response);
+ System.out.println("Response2 promptTokens: " + response.promptTokens());
+ System.out.println("Response2 completionTokens: " + response.completionTokens());
+ assertThat(response).isNotNull();
+ assertThat(response.promptTokens()).isLessThan(50);
+ assertThat(response.cacheWriteInputTokens()).isEqualTo(0);
+ assertThat(response.cacheReadInputTokens()).isGreaterThan(1000);
+ }
+}
\ No newline at end of file