Why Spring AI Fits Spring Boot Teams: The Patterns That Transfer Directly
▶ Watch on YouTube & subscribe to The Stack Underflow
An enterprise Java team gets asked to “add an AI feature” and the conversation stalls before a line of code gets written. Someone proposes a Python microservice. Someone else asks whether the team now needs to learn a new deployment pipeline, a new secrets story, a new way to write tests. The AI feature itself might be a week of work. The stack decision around it eats a quarter.
That stall is almost always unnecessary. If the team already ships Spring Boot services in production, they already own every pattern Spring AI needs — they just haven’t seen those patterns pointed at a model yet. Constructor injection, plain POJOs, starter dependencies, auto-configuration, and externalized, profile-scoped config are not new skills to acquire. They’re the same five things this team does every day, aimed at a ChatClient instead of a JdbcTemplate.
This matters for the adoption decision more than it matters for the code. A new stack means new CI/CD wiring, a new code review mental model, a new place secrets can leak, and a new test strategy nobody trusts yet. Reusing the stack you already run in production means none of that changes — which is the actual argument that gets an AI feature past a risk-averse platform team.
The one-sentence version: Spring AI is not a new framework bolted onto Spring Boot — it’s a set of starters and auto-configurations that follow the exact same conventions as
spring-boot-starter-weborspring-boot-starter-data-jpa, so every governance and testing pattern your team already trusts carries over unchanged.
The five-row map: Spring Boot habit → Spring AI equivalent
Before touching code, it helps to see the whole map at once. Every row on the left is something a Spring Boot developer does without thinking. Every row on the right is its direct Spring AI counterpart — same mechanism, different target.
| Spring Boot habit | Spring AI equivalent |
|---|---|
Constructor-inject a Repository | Constructor-inject a ChatClient.Builder |
A plain record or @Service class, no base class | The same plain @Service calling chatClient.prompt()...content() |
Add spring-boot-starter-web | Add spring-ai-starter-model-openai |
DataSource bean built by auto-configuration | OpenAiChatModel bean built by OpenAiChatAutoConfiguration |
spring.datasource.url in application.properties, profile-scoped | spring.ai.openai.chat.model in application.properties, profile-scoped |
Nothing in that table is a metaphor. Each right-hand cell is a real class or property, verified against Spring AI 2.0.0. Walk through the two rows that carry the most weight — dependency injection and auto-configuration — with actual code.
Row 1: constructor injection, unchanged
Here’s a service that talks to a database, the way you’d write it in any Spring Boot codebase:
@Service
public class OrderService {
private final OrderRepository repo;
OrderService(OrderRepository repo) {
this.repo = repo;
}
}
Here’s a service that talks to a model instead:
@Service
public class SupportService {
private final ChatClient chatClient;
SupportService(ChatClient.Builder builder) {
this.chatClient = builder.build();
}
public String answer(String question) {
return chatClient.prompt()
.user(question)
.call()
.content();
}
}
Same move: declare what you need in the constructor, let Spring wire it in, never call new. The only thing that changed is which bean got injected. Spring AI’s ChatClientAutoConfiguration publishes ChatClient.Builder as a prototype-scoped bean — each injection point gets its own freshly built instance, so two services can each customize their own ChatClient (different default system prompts, different advisors) without stepping on each other.
Notice, too, that SupportService has no special base class and no framework annotation beyond the ordinary @Service. It’s a plain, testable Spring bean — mock the ChatClient the same way you’d mock OrderRepository, and the test looks exactly like every other service test in the codebase.
Row 3 and 4: one starter, then auto-configuration does the rest
Turning on a capability in Spring Boot has always meant one thing: add a starter. Web needs spring-boot-starter-web. Persistence needs spring-boot-starter-data-jpa. A chat model needs its own starter, and the naming convention is identical:
// build.gradle.kts
dependencies {
implementation("org.springframework.ai:spring-ai-starter-model-openai:2.0.0")
}
That one line is the entire integration step. Once it’s on the classpath, OpenAiChatAutoConfiguration takes over — it’s annotated @ConditionalOnProperty(prefix = "spring.ai.model.chat", name = "openai", matchIfMissing = true), so it activates automatically unless you’ve configured a different chat model. It builds the OpenAiChatModel bean and hands out the ChatClient.Builder your services inject. You never write new OpenAiChatModel(...) by hand, for the same reason you never write new HikariDataSource() by hand when spring-boot-starter-jdbc is on the classpath — auto-configuration assembled the bean from config it read out of your properties file.
Row 5: externalized, profile-scoped config — with a real 1.0.3 → 2.0.0 delta
The API key and the model name don’t belong in code; they belong in application.properties, pulled from the environment, exactly like a datasource URL:
spring.ai.openai.api-key=${OPENAI_API_KEY}
spring.ai.openai.chat.model=gpt-5-nano
Because this is ordinary Spring config, it’s profile-scoped the same way anything else is — flip a whole feature (RAG on, MCP off) per environment with a spring.config.activate.on-profile block, the same mechanism you already use for datasource URLs per environment.
If you learned Spring AI from an older course or tutorial, watch this one carefully:
Version delta, 1.0.3 → 2.0.0: the older, course-taught property path was nested under
Options—spring.ai.openai.chat.options.model. In Spring AI 2.0.0,OpenAiChatProperties.Options.getModel()and the siblingOptionsgetters are@Deprecated(since = "2.0.0", forRemoval = true). The current path is the flattened one shown above, directly underOpenAiChatProperties:spring.ai.openai.chat.model. If you copy a snippet from a pre-2.0 source, this is the property most likely to be stale.
The seam this all lives in
Zoomed out, every pattern above lives inside one layer — the part Spring AI actually owns, sandwiched between the code you write and the model provider underneath:
┌───────────────────────────────────────────┐
│ YOUR CODE — @Service, constructor DI │
│ (unchanged: same beans, same tests) │
├───────────────────────────────────────────┤
│ SPRING AI — the seam │
│ starter → auto-configuration → beans │
│ (ChatClient.Builder, OpenAiChatModel) │
│ externalized, profile-scoped config │
├───────────────────────────────────────────┤
│ MODEL — swappable provider │
│ (OpenAI today, another provider tomorrow) │
└───────────────────────────────────────────┘
That’s the whole architectural claim: swap the bottom layer for a different provider by swapping a starter dependency and a property value, and the top layer — your code, your tests, your reviews — never has to know.
Prerequisites worth stating plainly
Spring AI 2.0.0 is built on Spring Boot 4.0 / Spring Framework 7.0 and requires Java 21 to build (Java 17 remains only the Spring Boot 4 runtime floor, not the build requirement). If your build is still pinned to Java 17, budget the JDK upgrade before the starter dependency, not after.
How to apply this right now
- Confirm your build toolchain is Java 21 before adding any Spring AI starter — this is a build-time requirement for Spring AI 2.0.0 / Spring Boot 4.0, not an optional nicety.
- Add exactly one starter —
org.springframework.ai:spring-ai-starter-model-openai:2.0.0— the same way you’d addspring-boot-starter-web. Resist the urge to hand-wire anOpenAiChatModelyourself; let auto-configuration do it. - Inject
ChatClient.Builder, neverChatClientdirectly, into the constructor of a plain@Service. Call.build()once in the constructor to get an immutableChatClientyou reuse across requests. - Put the API key and model name in
application.properties, sourced from an environment variable, using the flattenedspring.ai.openai.*path — not the deprecatedspring.ai.openai.chat.options.*path. - Profile-scope the config the same way you already profile-scope your datasource — separate
application-dev.properties/application-prod.propertiesblocks if your model, temperature, or provider should differ by environment. - Test the service like any other collaborator. Mock the injected
ChatClient(or the underlyingChatModel) in a unit test; save a real API call for a narrow, tagged integration test.
Common misconceptions
“Spring AI needs its own deployment pipeline.” It doesn’t. It’s a Spring Boot starter plus auto-configuration. If your CI/CD pipeline already builds and deploys a Spring Boot jar, it already knows how to build and deploy a Spring AI service — no new pipeline stage required.
“You have to construct the model client yourself to configure it.” You don’t, and doing so throws away the auto-configuration. Configure the model through application.properties (or a ChatClient.Builder customization inside your own @Bean method if you need programmatic control) — OpenAiChatAutoConfiguration builds the underlying OpenAiChatModel bean for you.
“Testing AI-calling code requires a different testing strategy.” A service that calls ChatClient is a plain Spring bean with an injected collaborator, same as a service that calls a repository. Mock the collaborator, assert on behavior, same test style your team already uses.
“The nested spring.ai.openai.chat.options.* properties still work fine, so it doesn’t matter which path I use.” They’re deprecated with forRemoval = true as of 2.0.0. They still function today, but code and config written against them is accruing migration debt on a clock you don’t control — use the flattened path now.
Frequently asked questions
Do I need Spring AI experience to follow this, or just Spring Boot experience? Just Spring Boot. That’s the entire point of this episode — every mechanism (constructor injection, starters, auto-configuration, externalized config) is one you already use. Spring AI adds new classes to those mechanisms, not new mechanisms.
What Java and Spring Boot version does Spring AI 2.0.0 require? Java 21 to build, and Spring Boot 4.0 / Spring Framework 7.0 as the platform. Java 17 is only the Spring Boot 4 runtime floor — it is not sufficient for building against Spring AI 2.0.0.
Why is ChatClient.Builder injected instead of ChatClient itself?
ChatClient.Builder is published as a prototype-scoped bean by ChatClientAutoConfiguration, meaning each injection point receives its own fresh builder instance. That lets different services in the same application configure their own default system prompt, advisors, or tools without any of them colliding — something a shared singleton ChatClient couldn’t do cleanly.
What does OpenAiChatAutoConfiguration actually do, mechanically?
It’s conditional on spring.ai.model.chat=openai (or no property set at all, since matchIfMissing = true), and when active, it constructs the OpenAiChatModel bean from your spring.ai.openai.* properties and makes the ChatClient.Builder available for injection. It’s the same category of class as DataSourceAutoConfiguration — you rarely open it, but it’s why the bean exists in your context.
Is spring.ai.openai.chat.options.model really deprecated, or is that just older course material being outdated?
Both things are true. Older course material (built against Spring AI 1.0.3) taught the nested spring.ai.openai.chat.options.model path. In 2.0.0, that path is genuinely @Deprecated(since = "2.0.0", forRemoval = true) on the framework side — this isn’t a stylistic preference, it’s a real API change you need to migrate for.
Does using Spring AI mean my security/compliance review process has to change? No — that’s the enterprise argument this episode makes. The API key is an environment variable read through the same secrets-management path as any other credential. The service is a normal Spring bean reviewed the same way. Nothing about adopting Spring AI requires a new governance process on top of the one your Spring Boot services already pass.
Where this fits in the series
This is Episode 2 of Spring AI for Enterprise Java. The previous episode laid out the three-layer architecture — your code, Spring AI, the model — that this episode zoomed into. The next episode opens up ChatClient itself and walks a request through the full .prompt().user().call().content() lifecycle, end to end.
Runnable code for everything shown here lives in the 02-why-spring-boot-teams module of the spring-ai-enterprise-examples repo — clone it, drop in your own OPENAI_API_KEY, and run it against a real model.
Spring AI moves fast; this series is pinned to 2.0.0, and update notes live at thestackunderflow.com/spring-ai. If you’d rather watch the five-row map get built live, subscribe to @TheStackUnderflow on YouTube — the video for this episode is in production now.
Found this useful? The deep version lives on YouTube — new breakdowns of how AI dev tools actually work, weekly.
Subscribe on YouTube →