Spring AI ChatClient API: The Fluent Heart of AI Integration
I am a developer who loves Java, Spring, Quarkus, Micronaut, Open source, Microservices, Cloud
Search for a command to run...
I am a developer who loves Java, Spring, Quarkus, Micronaut, Open source, Microservices, Cloud
This is a much-needed abstraction for the Java ecosystem. Python devs have had clean LLM client libraries for a while, but Java teams often end up writing boilerplate HTTP wrappers that are painful to maintain. The fluent API pattern makes a huge difference for readability, especially when you're chaining prompts with function calling or RAG retrieval steps. I'm curious how Spring AI handles streaming responses and backpressure — that's usually where fluent APIs start to get tricky with LLMs.
Spring AI Tool Calling: From Chatbot to AI Agent with @Tool Your AI is smart. It knows an enormous amount. But it's frozen. It doesn't know what time it is right now. It doesn't know what's on your ca
Spring AI Advisors API Explained Series: Spring AI Complete Course — Lecture 4 of 12Reading Time: 8 minutesLevel: Intermediate Most developers stop at ChatClient. That is enough for demos. It is not
Working with Multiple AI Models in Spring AI Spring AI Complete Course — Lecture 3 of 12Previous: Lecture 2 — ChatClient API | Next: Lecture 4 — Advisors API In production AI applications, you rarel
What is Spring AI? — Why Java Developers Need This in 2026 Every AI tutorial you see is in Python. LangChain, LlamaIndex, OpenAI SDK — all Python. But here's the uncomfortable truth: 80% of enterprise backends run Java. So who's building AI into thos...
If you've ever tried integrating AI models into a Java application, you know the pain. HTTP clients, API keys scattered everywhere, vendor-specific SDKs that never quite fit. What should take minutes takes days.
Spring AI's ChatClient API changes everything. It's a fluent, intuitive interface that makes calling AI models feel as natural as calling any other Spring service. In this post, we'll dive deep into the ChatClient API — the core building block that everything else in Spring AI builds upon.
At its core, ChatClient is Spring AI's abstraction over AI model communication. Think of it as the RestTemplate or WebClient for AI — but designed specifically for the conversational nature of LLMs.
The API follows three principles:
@Bean
ChatClient chatClient(ChatModel chatModel) {
return ChatClient.builder(chatModel)
.defaultSystem("You are a helpful Spring Boot expert")
.build();
}
Notice something beautiful here? No API keys. No HTTP configuration. Just the ChatModel — which Spring Boot has already autoconfigured from your application.properties.
Spring AI's autoconfiguration creates a prototype ChatClient.Builder bean. Inject it and build:
@RestController
class MyController {
private final ChatClient chatClient;
public MyController(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
}
}
Here's where ChatClient shines. The fluent API follows a natural left-to-right flow:
String response = chatClient.prompt()
.user("Explain Spring Boot in one sentence")
.call()
.content();
Breaking it down:
Need more than just text? Use .chatResponse() instead:
ChatResponse response = chatClient.prompt()
.user("Explain Spring Boot")
.call()
.chatResponse();
// Access metadata
Generation generation = response.getResult();
String finishReason = generation.getMetadata().getFinishReason();
int tokenCount = generation.getMetadata().getUsage().getTotalTokens();
Best for: Quick operations where you need the complete answer before continuing.
String answer = chatClient.prompt()
.user("What is dependency injection?")
.call()
.content();
Best for: Chat interfaces, long-form content, real-time updates.
Flux<String> tokens = chatClient.prompt()
.user("Write a detailed Spring Boot tutorial")
.stream()
.content();
// Subscribe and display tokens as they arrive
tokens.subscribe(System.out::println);
The beauty? Just swap call() for stream() and you get a reactive Flux of tokens. Spring AI handles all the Server-Sent Events (SSE) complexity.
This is where ChatClient becomes production-ready. Map AI responses directly to Java objects:
record Movie(String title, String director, int year) {}
Movie recommendation = chatClient.prompt()
.user("Recommend a classic programming movie")
.call()
.entity(Movie.class);
System.out.println(recommendation.title()); // "The Social Network"
Behind the scenes, Spring AI:
No more regex parsing. No more manual JSON mapping.
Real applications often need multiple AI models. Spring AI makes this elegant with @Qualifier:
@Configuration
class ChatClientConfig {
@Bean
@Qualifier("openai")
ChatClient openAiClient(ChatModel openAiModel) {
return ChatClient.builder(openAiModel)
.defaultSystem("You are an expert coder")
.build();
}
@Bean
@Qualifier("anthropic")
ChatClient anthropicClient(ChatModel anthropicModel) {
return ChatClient.builder(anthropicModel)
.defaultSystem("You are a creative writer")
.build();
}
}
// Usage
@Service
class AIService {
@Autowired
@Qualifier("openai")
private ChatClient openAiClient;
@Autowired
@Qualifier("anthropic")
private ChatClient anthropicClient;
}
Set application-wide behavior in the builder:
return ChatClient.builder(chatModel)
.defaultSystem("You are a helpful assistant. Be concise.")
.defaultOptions(ChatOptions.builder()
.temperature(0.7)
.build())
.build();
For debugging, intercept the ChatClient calls with an Advisor (covered in Lecture 4):
chatClient.prompt()
.advisors(new LoggingAdvisor())
.user("Hello")
.call();
Wrap calls in try-catch for model-specific exceptions:
try {
return chatClient.prompt()
.user(userInput)
.call()
.content();
} catch (AiException e) {
// Handle rate limits, token limits, etc.
return "Service temporarily unavailable";
}
Here's a production-ready controller with multiple model configs and streaming:
@RestController
@RequestMapping("/api/ai")
public class AIController {
private final ChatClient defaultClient;
private final ChatClient streamingClient;
public AIController(
@Qualifier("openai") ChatClient defaultClient,
@Qualifier("groq") ChatClient streamingClient) {
this.defaultClient = defaultClient;
this.streamingClient = streamingClient;
}
// Sync endpoint for quick responses
@GetMapping("/ask")
public ResponseEntity<String> ask(@RequestParam String question) {
String answer = defaultClient.prompt()
.user(question)
.call()
.content();
return ResponseEntity.ok(answer);
}
// Streaming endpoint for chat-like experience
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> stream(@RequestParam String question) {
return streamingClient.prompt()
.user(question)
.stream()
.content();
}
// Entity mapping endpoint
@PostMapping("/analyze")
public ResponseEntity<Analysis> analyze(@RequestBody String text) {
Analysis result = defaultClient.prompt()
.user("Analyze this text: " + text)
.call()
.entity(Analysis.class);
return ResponseEntity.ok(result);
}
public record Analysis(String sentiment, List<String> keyPoints) {}
}
Now that you understand the ChatClient API, the next lecture covers Working with Multiple AI Models — how to configure, compare, and combine different model providers in a single application.
Series: Spring AI Complete Course Next: Lecture 3 — Working with Multiple AI Models Code samples: [GitHub Repository Link]
#springai #java #springboot #ai #chatclient #llm #tutorial