In the previous article, we ran a Spring AI application in about twenty lines of Java. It worked. A ChatClient appeared out of nowhere, we called .prompt().user(...).call().content(), and Claude answered.
That’s the magic of Spring Boot autoconfiguration — and also the danger of it.
When things work without explanation, you’re fine right up until you need to debug something, customise something, or explain to a colleague what exactly is happening. Then you’re stuck. I’ve been there more times than I care to admit — staring at a NoSuchBeanDefinitionException at 4pm on a Friday, completely unable to explain why a bean that “should just be there” isn’t. (It was a missing property. It’s always a missing property.)
So before we go any further into Spring AI’s features, let’s open the hood. This article covers everything you need to set up a proper Spring AI project and understand what Spring is wiring up on your behalf. By the end, you’ll be able to look at any Spring AI autoconfiguration and know exactly what it’s doing.
We’ll also cover something promised in article 1: how to switch from Anthropic to OpenAI, or run a completely free local model via Ollama or LMStudio.
What You’ll Need
Before anything else — the prerequisites:
- Java 25. Java 21 works too if you’re not on the latest yet.
- Maven 3.9+ or Gradle 8.8+. Either works. We’ll use Maven in the examples; the equivalent Gradle files are in the GitHub repository.
- An IDE. IntelliJ IDEA works best for Java. VS Code with the Java extension pack is fine too.
- A model provider account. We’ll cover your options — including free ones — in a moment.
That’s genuinely it. No Docker, no Kubernetes, no local infrastructure to wrestle with before you’ve even written a line of code.
Choosing Your Model Provider
This is the first real decision you’ll make in a Spring AI project. Spring AI abstracts over model providers beautifully — but you still need to pick one to start.
Here’s the honest picture:
| Provider | Cost | Setup effort | Good for |
|---|---|---|---|
| Anthropic (Claude) | Pay per token (~$5 free credits) | API key only | This series’ default |
| OpenAI | Pay per token (some free credits) | API key only | Widely documented |
| Ollama | Free | Install + pull a model | Local dev, zero API costs |
| LMStudio | Free | Install + download a model | Local dev with a GUI |
We’ll use Anthropic as the default throughout this series — specifically claude-haiku-4-5-20251001, the most affordable model in the Claude family. It’s perfectly capable for everything we’ll build, and it keeps your costs minimal while learning.
If you’d rather pay nothing, Ollama is the best free alternative. Models run locally — no API key, no usage limits, no bill at the end of the month. The trade-off is that local models are generally less capable than frontier models and need reasonable hardware (8GB RAM minimum, 16GB recommended). For learning Spring AI, they’re more than sufficient.
Setting Up Anthropic
Head to console.anthropic.com, create an account, and generate an API key under Settings → API Keys. New accounts get approximately $5 in free credits — no payment method required.
Store the key as an environment variable. Never put it directly in application.properties — you will accidentally commit it eventually. We all have. It’s fine. Just don’t do it again.
Mac/Linux:
| |
Windows (PowerShell):
| |
Setting Up OpenAI
Head to platform.openai.com, create an account, and generate an API key under API Keys. New accounts get a small amount of free credits — enough to follow along with the early articles.
Store the key as an environment variable, same as with Anthropic:
Mac/Linux:
| |
Windows (PowerShell):
| |
For a more permanent solution, add it to your shell profile (~/.zshrc, ~/.bashrc) or use your IDE’s run configuration environment variables. We’ll cover production secrets management properly in article 13.
Setting Up Ollama (Free Alternative)
Install Ollama from ollama.com. Then pull a model:
| |
That’s it. Ollama runs as a local HTTP server on http://localhost:11434. No API key needed, no account, no credit card. Make sure it’s running before starting your Spring application — ollama serve if it’s not already running as a background service.
Setting Up LMStudio (Free Alternative with a GUI)
If you prefer a graphical interface, LMStudio is a good option. Download it, browse the built-in model library, download a model, and start the local server from the app. It exposes an OpenAI-compatible API on http://localhost:1234 by default — which means you can use Spring AI’s OpenAI provider to talk to it, with a custom base URL pointing at your machine.
Creating the Project
The fastest way is start.spring.io. Select:
- Project: Maven (or Gradle)
- Language: Java
- Spring Boot: 4.0.x
- Java: 25
- Dependencies: Anthropic Claude AI (or whichever provider you’re using)
Hit Generate, unzip, open in your IDE.
If you’re setting up manually, here’s the complete pom.xml. This is the nearly the same file as from article 1. We only added the Spring Starter for OpenAI as dependency
| |
The Gradle equivalent is in the GitHub repository.
The Spring AI BOM
Notice the dependencyManagement block. Spring AI uses a Bill of Materials — a BOM — to manage the versions of all its internal modules consistently. Declare the BOM version once, and every Spring AI dependency you add inherits it automatically. No <version> attributes on individual dependencies, no version mismatch headaches.
The same pattern Spring Boot itself uses. Keep the BOM version in a property — ${spring-ai.version} — so upgrading the entire Spring AI dependency set is a one-line change.
The Starter System
Spring AI follows Spring Boot’s starter convention. Each model provider has a dedicated starter:
| Provider | Starter artifact |
|---|---|
| Anthropic | spring-ai-starter-model-anthropic |
| OpenAI | spring-ai-starter-model-openai |
| Ollama | spring-ai-starter-model-ollama |
spring-ai-starter-model-google-genai | |
| Azure OpenAI | spring-ai-starter-model-azure-openai |
| Mistral | spring-ai-starter-model-mistral-ai |
| DeepSeek | spring-ai-starter-model-deepseek |
Each starter pulls in the necessary dependencies and — critically — an autoconfiguration class that wires up the right Spring beans when it finds the correct properties on the classpath. You only need one starter to get a working chat model. But you can include multiple starters if you want to work with multiple providers simultaneously — and we will.
What Autoconfiguration Actually Does
Spring Boot’s autoconfiguration fires @ConditionalOn* checks at startup — is a certain class on the classpath? Is a certain property set? If all conditions pass, the autoconfiguration registers its beans into the application context. You can always override any autoconfigured bean by defining your own. It’s “sensible defaults, not forced defaults.”
When Spring AI’s Anthropic autoconfiguration activates, it creates two key beans:
AnthropicChatModel — the model abstraction. This implements Spring AI’s ChatModel interface, the common contract for all chat models regardless of provider. It’s built using Spring AI 2.0’s new builder — internally backed by the official Anthropic Java SDK rather than a hand-rolled HTTP client. You’ll rarely construct this manually, but knowing it exists matters.
ChatClient.Builder — not Anthropic-specific, but auto-registered by Spring AI’s core autoconfiguration once it detects a ChatModel bean. This is the builder you inject into your application code.
So when you write this:
| |
The ChatClient.Builder you’re injecting was registered by Spring AI’s autoconfiguration, backed by an AnthropicChatModel. Two layers of abstraction, wired automatically. That’s the chain.
The Model API Layer
ChatModel — the interface AnthropicChatModel implements — is Spring AI’s lowest-level chat abstraction:
| |
A Prompt in, a ChatResponse out. You’ll rarely use ChatModel directly in application code — ChatClient is the higher-level, more ergonomic API. But knowing ChatModel exists matters for two reasons: it’s what you inject when writing tests (easier to mock), and it’s what autoconfiguration actually creates. ChatClient is built on top of ChatModel.
ChatModel is the engine. ChatClient is the dashboard.
Configuring Your Application
Spring AI is configured through Spring Boot’s standard property system. We use application.properties throughout this series rather than YAML — not because YAML is wrong, but because properties files are easier to copy-paste individual lines from, and there’s no indentation to misalign. For complex nested configuration YAML is genuinely nicer; for tutorial examples, properties win on clarity.
Please note that if you are using a Spring AI 1.x version, the properties are defined as
spring.ai.<provider>.chat.options.xxxinstead ofspring.ai.<provider>.chat.xxx. See here for details
Anthropic
| |
We’re deliberately using
claude-haiku-4-5-20251001— Anthropic’s most affordable model — throughout this series. It’s more than capable for learning and experimentation. When you move to production, swap in a more powerful model by changing this single property.
OpenAI
Swap the dependency in pom.xml to spring-ai-starter-model-openai, then:
| |
Your application code doesn’t change. At all.
Ollama (Free, Local)
Swap the dependency to spring-ai-starter-model-ollama:
| |
No API key. Make sure Ollama is running before starting your application.
If you decide to not specify a model, the default model will be used, which is mistral.
LMStudio (Free, Local)
LMStudio exposes an OpenAI-compatible API, so use the OpenAI provider with a custom base URL:
| |
The api-key value is required by the provider but ignored by LMStudio — any non-empty string works.
Switching Providers Is One Dependency Swap
This should be familiar to all Spring developers: Your application code — the ChatClient calls, the prompt construction, the response handling — is identical regardless of which provider you’re using. The only things that change are the starter dependency and a handful of properties.
That’s what we have become to expect from Spring, and it genuinely works that way. We’ll see it in practice throughout the series.
Running Two Providers Simultaneously
Here’s where it gets interesting. Sometimes you want both Anthropic and OpenAI active in the same application — perhaps using Claude for creative tasks and GPT-4o-mini for structured extraction, or routing between providers based on cost. Spring AI handles this cleanly.
When multiple ChatClient beans exist, Spring needs to know which one to inject when no qualifier is specified. The cleanest solution is @Primary — mark your default provider’s bean with it, and Spring will use it for unqualified injection points. Only the secondary provider needs a @Qualifier. This maps naturally to how most applications actually work: one provider for general use, another for specific tasks like structured extraction or cost-sensitive operations.
If there’s genuinely no default — both providers are equally specific — use @Qualifier on both beans and both injection points. Either way, constructing your ChatClient beans from the typed model beans (AnthropicChatModel, OpenAiChatModel) rather than the generic ChatClient.Builder avoids autoconfiguration ambiguity entirely.
| |
Both providers need their properties set:
| |
Then define a ChatClient bean for each, backed by its respective model:
| |
Inject by qualifier wherever you need them:
| |
Standard Spring qualifier injection. No Spring AI-specific magic involved.
One thing to be aware of: when multiple ChatClient.Builder beans exist, Spring’s autoconfiguration for ChatClient.Builder may complain about ambiguity. The solution is to always construct your ChatClient beans explicitly from the typed model beans (AnthropicChatModel, OpenAiChatModel) rather than from the generic ChatClient.Builder.
Our main application class will look like this:
| |
If you enter no arguments, the application will use Anthropic. If you enter some text, it will send it to OpenAI.
Customising the ChatClient Bean
For single-provider applications, the cleaner pattern is a dedicated configuration class rather than constructing ChatClient inline in each component:
| |
Any component in your application can now inject ChatClient directly. The defaultSystem call sets a system prompt that applies to every conversation through this ChatClient — we’ll cover system prompts properly in article 3.
Overriding Autoconfiguration
Autoconfiguration is a default, not a mandate. If you need to go beyond what properties allow — custom retry behaviour, a specific base URL, additional SDK configuration — you can construct the model bean manually using Spring AI 2.0’s builder:
| |
The @Primary annotation tells Spring to prefer this bean over the autoconfigured one when there’s ambiguity. For most applications, property-based configuration is sufficient and the cleaner approach — but it’s useful to know you can drop down to the builder when you need to.
Debugging Autoconfiguration
At some point, something won’t wire up. Spring Boot has a built-in tool for exactly this situation.
Add this to application.properties:
| |
This produces a condition evaluation report at startup — a full list of every autoconfiguration class, whether it activated, and if not, why not. It’s verbose, but when you’re trying to work out why your ChatClient bean isn’t being created, it’s invaluable.
For Spring AI specifically, the property prefix to look for is spring.ai. If autoconfiguration isn’t firing, it’s almost always either a missing property or a missing starter. The condition report will tell you which — saving you that Friday afternoon.
Profiles for Multiple Environments
A common pattern is using Spring profiles to switch providers between environments — Ollama locally, Anthropic in production:
| |
application.properties:
| |
application-local.properties:
| |
application-prod.properties:
| |
Run locally with -Dspring.profiles.active=local. Deploy to production with -Dspring.profiles.active=prod. The application code never changes.
As usual, the source for this example is available on my GitHub.
What’s Next
You now have a properly structured Spring AI project, a clear understanding of what autoconfiguration wires up, the ability to switch providers with a dependency swap, and a multi-provider pattern ready to use when you need it.
In the next article, we go deep on ChatClient itself — the fluent API, prompt construction, system messages, default options, and how to structure a configuration that scales cleanly as your application grows. If .prompt().user(...).call().content() has been a bit of a black box so far, article 3 opens it up completely.
The engine is running. Time to learn the dashboard.
This is part 2 of a 13-part series.
