Prompt Caching in Spring AI with Anthropic Claude
Prompt caching lets Claude reuse the processing it already did on a prompt prefix instead of re-reading it from scratch on every request. For applications that send the same large system prompt, tool definitions, or growing conversation history on every call, this cuts both cost and latency. Spring AI exposes this directly on its Anthropic integration — you don’t hand-build cache_control JSON yourself; you declare a caching strategy on AnthropicChatOptions, and Spring AI inserts the right cache breakpoints for you.
1. Understanding Prompt Cache? What Are the Different Prompt Caching Strategies?
Under the hood, Claude’s prompt cache is a prefix match: the API hashes the prompt up to a marked breakpoint, and a later request with the identical prefix bytes reads that cached computation instead of reprocessing it. A cache write costs about 1.25× the normal input price; a cache read costs about 0.1× — so caching pays off once the same prefix is reused across two or more requests within the cache’s TTL (5 minutes by default, or 1 hour if configured). Spring AI packages this behind AnthropicCacheStrategy, an enum that controls which parts of the prompt get a cache breakpoint:
| Strategy | What gets cached |
|---|---|
NONE | No caching — the default if you don’t configure anything. |
SYSTEM_ONLY | Caches the system message content (1 breakpoint). |
TOOLS_ONLY | Caches tool definitions only (1 breakpoint). |
SYSTEM_AND_TOOLS | Caches both system message and tool definitions (2 breakpoints). |
CONVERSATION_HISTORY | Caches system, tools, and growing conversation messages (up to 4 breakpoints). |
Anthropic caps every request at 4 cache breakpoints, and Spring AI tracks that limit internally so it stops adding more once it’s reached. For a support bot with a large, mostly-static system prompt — the scenario in this article — SYSTEM_ONLY is the right fit: the system prompt is identical across requests, while each user’s question is unique and should stay outside the cached prefix.
2. Code Example
Add the Anthropic starter to a Spring Boot project:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-anthropic</artifactId>
<version>stable__jar__version</version>
</dependency>
This starter auto-configures an AnthropicChatModel bean, backed by the official anthropic-java SDK, that you can inject directly into your own classes — no separate caching dependency is needed, since AnthropicCacheOptions ships in the same artifact.
Point it at your API key and model in application.properties:
spring.ai.anthropic.api-key=${ANTHROPIC_API_KEY}
spring.ai.anthropic.chat.options.model=claude-sonnet-4-20250514
Spring AI reads ANTHROPIC_API_KEY from the environment at startup, so no key is hardcoded in the properties file, and the default model set here can still be overridden per-request via AnthropicChatOptions.builder().model(...), as the example below does.
2.1 Service Class
Now a service that sends a large, fixed system prompt with every request and enables SYSTEM_ONLY caching, calling the model twice to see the cache in action:
import com.anthropic.models.messages.Usage;
import org.springframework.ai.anthropic.AnthropicChatModel;
import org.springframework.ai.anthropic.AnthropicChatOptions;
import org.springframework.ai.anthropic.api.AnthropicCacheOptions;
import org.springframework.ai.anthropic.api.AnthropicCacheStrategy;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class SupportAssistant {
private static final String SYSTEM_PROMPT = """
You are a product support assistant for Acme Cloud Storage.
Pricing tiers: Free (5GB), Pro ($9/mo, 500GB), Business ($29/mo, 5TB).
API rate limit: 1000 requests/hour on Free, 10000 on Pro and above.
... (several thousand tokens of policies and troubleshooting steps) ...
""";
private final AnthropicChatModel chatModel;
public SupportAssistant(AnthropicChatModel chatModel) {
this.chatModel = chatModel;
}
public void askTwice() {
var options = AnthropicChatOptions.builder()
.model("claude-sonnet-4-20250514")
.cacheOptions(AnthropicCacheOptions.builder()
.strategy(AnthropicCacheStrategy.SYSTEM_ONLY)
.build())
.build();
ChatResponse first = chatModel.call(new Prompt(List.of(
new SystemMessage(SYSTEM_PROMPT),
new UserMessage("What's the free-tier storage limit?")), options));
printCacheUsage("First call ", first);
ChatResponse second = chatModel.call(new Prompt(List.of(
new SystemMessage(SYSTEM_PROMPT),
new UserMessage("How do I upgrade to the Pro plan?")), options));
printCacheUsage("Second call", second);
}
private void printCacheUsage(String label, ChatResponse response) {
Usage usage = (Usage) response.getMetadata().getUsage().getNativeUsage();
System.out.println(label
+ " -> cacheCreation=" + usage.cacheCreationInputTokens().orElse(0L)
+ ", cacheRead=" + usage.cacheReadInputTokens().orElse(0L));
}
}
cacheOptions(AnthropicCacheOptions.builder().strategy(AnthropicCacheStrategy.SYSTEM_ONLY).build()) is the only caching-specific line — it tells Spring AI to place one cache_control breakpoint on the system message when it builds the underlying Anthropic request. Both calls send the exact same SYSTEM_PROMPT string, which is what makes the second call eligible for a cache hit, since the breakpoint sits on the system message and the different UserMessage text in each call never affects whether the prefix matches; and because Spring AI’s ChatResponse wraps the Anthropic SDK’s own Usage object, casting getNativeUsage() back to com.anthropic.models.messages.Usage exposes the cache-specific cacheCreationInputTokens()/cacheReadInputTokens() counters — each an Optional<Long>, hence the .orElse(0L) default — that aren’t part of Spring AI’s provider-neutral usage model.
2.2 Application Startup
SupportAssistant is a Spring-managed bean, not a standalone class, so it needs a Spring Boot application to construct it and inject the auto-configured AnthropicChatModel. The main method below boots the context and runs askTwice() once on startup:
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class PromptCachingDemoApplication {
public static void main(String[] args) {
SpringApplication.run(PromptCachingDemoApplication.class, args);
}
@Bean
CommandLineRunner runDemo(SupportAssistant supportAssistant) {
return args -> supportAssistant.askTwice();
}
}
@SpringBootApplication enables auto-configuration, which is what registers the AnthropicChatModel bean from the starter dependency based on the spring.ai.anthropic.* properties; SpringApplication.run(...) in main starts the application context, constructing SupportAssistant and injecting the AnthropicChatModel into its constructor automatically; and the CommandLineRunner bean is Spring Boot’s standard hook for running code once right after the context finishes starting, receiving the already-constructed SupportAssistant as a parameter and calling askTwice() on it, which triggers the two chat calls and prints the cache usage lines shown next.
2.3 Code Output
Running this class (mvn spring-boot:run, or the application’s main class from your IDE) prints:
First call -> cacheCreation=1842, cacheRead=0 Second call -> cacheCreation=0, cacheRead=1842
The first call has no prior cache entry to read, so Claude processes the system prompt normally and writes it to cache — cacheCreation=1842, cacheRead=0. The second call, made within the same 5-minute TTL with a byte-identical system prompt, skips reprocessing those 1,842 tokens entirely and reads them from cache instead — cacheCreation=0, cacheRead=1842 — while the new user question is still answered normally since it sits after the cache breakpoint. In production, that second call is billed at roughly a tenth of the input cost of the first for that portion of the prompt.
3. Conclusion
Spring AI turns Claude’s prompt caching from a manual cache_control-placement exercise into a one-line configuration choice: pick an AnthropicCacheStrategy that matches what’s actually stable in your prompt — the system message, tool definitions, or the whole growing conversation — and let Spring AI place the breakpoints. The payoff only shows up when requests genuinely share a prefix, so the one habit worth keeping is checking cacheCreationInputTokens/cacheReadInputTokens after any change to prompt assembly, the same way you’d check a cache hit rate anywhere else in a system.




