Your First Spring AI Application
In this tutorial, you will build a complete, working Spring AI application from scratch. By the end, you will have a REST endpoint that accepts a user message, sends it to an AI model, and returns an intelligent response—using only Spring Boot and a few lines of Java code.
Along the way, you will learn how Spring AI abstracts away the complexity of calling large language model APIs directly, so you can focus on building features instead of managing HTTP clients and JSON parsing. This project will be your foundation for adding chat memory, retrieval‑augmented generation, tool calling, and enterprise capabilities later.
You will build: a simple /api/chat endpoint that communicates with OpenAI (or a local model via Ollama).
You will learn:
- How to set up a Spring Boot project with Spring AI dependencies
- How to configure an AI model provider
- How to use
ChatClientto send prompts and receive responses - How to structure your code for testability and portability
- Basic error handling and logging for AI calls
Prerequisites​
Make sure your development environment meets the following:
- Java 21 or later – Spring AI requires Java 21+.
- Spring Boot 3.3.x (or newer) – We will use Spring Boot 3.3.5.
- Maven or Gradle – for dependency management.
- IDE – IntelliJ IDEA Community/Ultimate or VS Code with Spring extensions.
- API Key – For OpenAI, you need an API key from platform.openai.com. For local development without an internet connection, you can use Ollama (free, local). Other providers (Azure OpenAI, Anthropic Claude, Google Gemini, DashScope) work similarly and are covered in the Providers section.
If you haven’t yet, follow the Spring AI Installation Guide to set up your environment and verify that you can create a Spring Boot project with Spring AI dependencies.
Create a New Spring Boot Project​
The fastest way to create a project is via Spring Initializr.
Using Spring Initializr​
- Go to start.spring.io
- Choose:
- Project: Maven (or Gradle)
- Language: Java
- Spring Boot: 3.3.5 or later
- Group:
com.example - Artifact:
ai-demo - Dependencies: Spring Web, Spring AI (OpenAI) – if available; otherwise add manually.
If the Spring AI starter isn’t listed, download the project with only Spring Web, then add the dependency manually as shown below.
Manual Dependency Addition​
For Maven, edit pom.xml:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
</dependencies>
Add the Spring Milestones repository if you’re using a milestone version:
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots><enabled>false</enabled></snapshots>
</repository>
</repositories>
For Gradle, build.gradle:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter'
}
repositories {
mavenCentral()
maven { url 'https://repo.spring.io/milestone' }
}
Configure Spring AI​
Spring AI uses auto‑configuration based on properties in application.yml or application.properties. Never hardcode API keys—use environment variables.
For OpenAI​
Create src/main/resources/application.yml:
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4o
temperature: 0.7
Set the environment variable OPENAI_API_KEY with your actual key.
For Local Ollama (alternative)​
If you prefer a local model without API costs, install Ollama and pull a model:
ollama pull llama3
Then configure:
spring:
ai:
ollama:
base-url: http://localhost:11434
chat:
options:
model: llama3
temperature: 0.7
You can switch between providers by changing the dependency (e.g., spring-ai-azure-openai-spring-boot-starter) and updating the configuration properties. The application code remains identical because it depends only on abstractions like ChatClient and ChatModel.
Understanding ChatClient​
ChatClient is the primary entry point for interacting with AI models in Spring AI. It provides a fluent builder API to construct prompts, apply advisors, and handle responses.
- ChatClient – Orchestrates the call, applies advisors (for logging, memory, RAG, etc.), and returns a
ChatResponse. - ChatModel – The portable abstraction that represents any large language model. The provider adapter (OpenAI, Ollama, …) implements this interface.
Because your code talks only to ChatClient and never to the concrete provider class, you can change providers without touching business logic.
Create the AI Service​
Now you will write a simple service that uses ChatClient to send a user message and retrieve the AI’s response.
Create a package com.example.aidemo.service and add the class ChatService.java:
package com.example.aidemo.service;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
@Service
public class ChatService {
private final ChatClient chatClient;
public ChatService(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
public String chat(String userMessage) {
return chatClient.prompt()
.user(userMessage)
.call()
.content();
}
}
Explanation:
ChatClient.Builderis auto‑configured by Spring AI. We inject it and call.build()to create a defaultChatClientinstance..prompt().user(...)creates a prompt with only a user message..call()sends the request to the model..content()extracts the text response.
That’s it—no manual HTTP calls, no JSON parsing.
Create a REST Controller​
Now expose this service via a simple REST endpoint.
Create com.example.aidemo.controller.ChatController.java:
package com.example.aidemo.controller;
import com.example.aidemo.service.ChatService;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/chat")
public class ChatController {
private final ChatService chatService;
public ChatController(ChatService chatService) {
this.chatService = chatService;
}
@GetMapping
public String chat(@RequestParam String message) {
return chatService.chat(message);
}
}
This endpoint accepts a query parameter message and returns the model’s response as plain text.
Run the Application​
- Start the application from your IDE or with
mvn spring-boot:run. - Once the server is up, test it with
curl:
curl -X GET "http://localhost:8080/api/chat?message=Hello,%20who%20are%20you?"
Or open a browser:
http://localhost:8080/api/chat?message=Hello, who are you?
Expected response (example):
I'm an AI language model created by OpenAI. How can I help you today?
If you see a response, congratulations—you have built your first Spring AI application!
Understanding the Request Flow​
The following sequence diagram illustrates the complete lifecycle of a request:
Spring AI handles all serialization, deserialization, and error mapping, so your code stays clean.
Improving the Prompt​
The model’s behavior can be dramatically improved by providing a system prompt—a message that instructs the model how to behave without being visible to the user.
Update ChatService to include a system message:
public String chatWithSystemPrompt(String userMessage) {
return chatClient.prompt()
.system("You are a helpful customer support agent for a tech company. Answer concisely.")
.user(userMessage)
.call()
.content();
}
Now the model will adopt a consistent tone and role. You can also use PromptTemplate for reusable, parameterized prompts (explored in the Prompt guide).
Handling Errors​
LLM calls can fail for many reasons: invalid API key, timeout, rate limiting, or temporary provider outages. Never let these failures crash your application.
Wrap the AI call in a try‑catch and return a user‑friendly error message:
public String chatSafely(String userMessage) {
try {
return chatClient.prompt().user(userMessage).call().content();
} catch (Exception e) {
// Log the real error
log.error("AI call failed", e);
return "Sorry, I’m having trouble answering right now. Please try again later.";
}
}
For production, combine this with Spring Retry or a circuit breaker. The Enterprise AI section covers robust error handling patterns.
Logging and Debugging​
Enable debug logging to see the full prompt sent to the model and the raw response. Add to application.yml:
logging:
level:
org.springframework.ai: DEBUG
Now you can observe the token usage, model name, and the exact messages exchanged. This is invaluable when debugging why a response didn’t meet expectations.
For production observability—tracing, metrics, dashboards—refer to the Observability guide.
Next Steps​
This application is a solid starting point. From here, you can evolve it into a full‑featured AI platform:
- Chat Memory – Remember previous messages for multi‑turn conversations.
- Structured Output – Map AI responses directly to Java POJOs.
- Tool Calling – Let the model trigger your methods (database lookup, API calls).
- RAG – Ground responses in your own documents.
- AI Agents – Build autonomous decision‑making loops.
- Streaming – Deliver token‑by‑token responses for a chat‑like experience.
Each of these topics is covered in depth in the Framework and Tutorials sections.
Best Practices​
- Keep prompts externalised (e.g., in
application.ymlor separate files) rather than hardcoded in Java. - Never expose API keys in source code; use environment variables or a secrets manager.
- Validate user input before sending it to the model.
- Use dependency injection – always inject
ChatClient.BuilderorChatClientinstead of creating instances manually. - Write integration tests with a mock
ChatModelto avoid calling real APIs during testing. - Code against the
ChatClientandChatModelinterfaces, not against provider‑specific classes, to preserve portability. - Monitor token consumption from the start; it helps you control costs and tune prompts.
Common Mistakes​
- Calling provider APIs directly – You lose portability, advisor support, and error handling. Always use
ChatClient. - Hardcoding prompts – Moving prompts to configuration simplifies tuning and A/B testing.
- Mixing business logic with AI logic – Keep the AI interaction isolated in a dedicated service.
- Ignoring error handling – LLM calls are unreliable by nature; always handle failures gracefully.
- Ignoring rate limits – Without throttling, you may hit provider limits and break your application.
- Creating provider lock‑in – Using a concrete
OpenAiChatModelmakes switching expensive. Use the abstraction.
What's Next​
Now that you have a working application, deepen your knowledge with these resources:
- Spring AI Architecture – Understand the framework’s design.
- ChatClient Guide – Master the fluent API.
- Prompt Engineering – Learn how to craft effective prompts.
- Tool Calling – Enable models to invoke your Java methods.
- Retrieval‑Augmented Generation (RAG) – Ground AI in your data.
- Spring AI Tutorials – Build complete projects step‑by‑step.
Key Takeaways​
- Spring AI’s
ChatClientlets you send prompts and receive responses with minimal code. - Configuration is done via
application.yml; provider switching is a matter of changing dependencies and properties. - The architecture separates
ChatClient(orchestration) fromChatModel(provider adapter). - A system prompt gives the model a consistent role and behaviour.
- Always handle failures gracefully and enable logging for debugging.
- Build on this foundation by adding memory, RAG, tools, and observability as your application grows.