← Janaka Academy

The Spring ecosystem, and what Spring AI can do now.

Twelve years of Spring Boot in four generations, then the part that changed fastest: Spring AI 2.0, which turns a language model into a Spring bean with tools, memory, retrieval, MCP and evaluation. Every section has code you can drop into a Boot 4 project.

Spring Boot 4.1 · Framework 7 Spring AI 2.0.1 Java 17+, 25 recommended Working examples

Part one

Spring Boot, generation by generation.

Spring Framework is older than most of its users' careers; Spring Boot is what made it the default way to build a Java service. Four generations, each tied to a Java baseline.

GenerationGABaselineWhat it is remembered for
Spring Boot 1.0April 2014Spring Framework 4 · Java 6+Convention over configuration, and an executable jar.
Spring Boot 2.0March 2018Spring Framework 5 · Java 8+Reactive web, Micrometer metrics, Java 8 as the floor.
Spring Boot 3.0November 2022Spring Framework 6 · Java 17+ · Jakarta EE 9Java 17, Jakarta namespaces, native images, observability.
Spring Boot 4.0 and Spring Framework 7November 2025Spring Framework 7 · Java 17+, Java 25 recommended · Jakarta EE 11Modular jars, null safety, API versioning, resilience in the core.
Spring AI 1.0 → 2.0May 2025 → June 2026Boot 3.4 → Boot 4.0/4.1Language models, tools, RAG and MCP as first-class Spring components.

Spring Boot 1.0

April 2014 Spring Framework 4 · Java 6+ overview ↑

Convention over configuration, and an executable jar.

Auto-configurationStartersEmbedded TomcatActuatorExecutable jar

Spring Boot 1.0 answered the question every Spring project had been asking since 2004: why does it take a day of XML to serve one HTTP endpoint? Auto-configuration inspects the classpath and configures what it finds; starters bundle coherent dependency sets; the embedded server turns a web application into a process you start with java -jar. Actuator shipped in 1.0 too, which is why health and metrics endpoints are a Spring reflex rather than an afterthought.

@SpringBootApplication                       // @Configuration + @EnableAutoConfiguration + @ComponentScan
public class Application {
    public static void main(String[] args) { SpringApplication.run(Application.class, args); }
}

@RestController
class HelloController {
    @RequestMapping("/") String hello() { return "Hello from Spring Boot 1.0"; }
}

Why it mattered. The deployment unit changed from a WAR dropped into a shared application server to a self-contained process. Every container and cloud practice that followed assumes that shape.

Spring Boot 2.0

March 2018 Spring Framework 5 · Java 8+ overview ↑

Reactive web, Micrometer metrics, Java 8 as the floor.

Spring WebFluxReactorMicrometerJava 8 baselineKotlin supportGradle plugin rewriteHTTP/2

Boot 2.0 arrived alongside Spring Framework 5 and made Java 8 the minimum, so lambdas and functional bean registration became idiomatic. WebFlux offered a non-blocking stack for the cases that needed it, and Micrometer became the metrics facade, which is how Spring applications learned to speak Prometheus. Spring Boot 2.3 later added buildpack images, layered jars and graceful shutdown; 2.4 through 2.7 refined configuration processing and Java 17 support while 3.0 was being prepared.

@RestController
class QuoteController {
    private final WebClient client = WebClient.create("https://api.example.com");

    @GetMapping("/quotes/{isin}")
    Mono<Quote> quote(@PathVariable String isin) {          // non-blocking end to end
        return client.get().uri("/quotes/{isin}", isin).retrieve().bodyToMono(Quote.class);
    }
}

Why it mattered. WebFlux solved concurrency for the few services that needed it at the cost of a different programming model. Most teams stayed on Spring MVC, and with virtual threads in Boot 3.2 that turned out to be the right call.

Spring Boot 3.0

November 2022 Spring Framework 6 · Java 17+ · Jakarta EE 9 overview ↑

Java 17, Jakarta namespaces, native images, observability.

Java 17 baselinejakarta.* packagesGraalVM native imagesMicrometer Observation and TracingProblem Details (RFC 7807)HTTP interface clientsDeclarative HTTP clients

Boot 3.0 was the largest migration since 2.0: Java 17 became the floor, every javax.* import became jakarta.*, and GraalVM native compilation went from experiment to supported feature, with start-up in tens of milliseconds. The Micrometer Observation API unified metrics and tracing so a single annotation produces both. Spring Boot 3.2 (November 2023) then delivered the operational win of the generation: virtual threads for Tomcat and every task executor with one property, plus RestClient and JdbcClient as synchronous, fluent replacements for RestTemplate and JdbcTemplate.

# application.properties, Spring Boot 3.2+
spring.threads.virtual.enabled=true          # Tomcat, @Async, @Scheduled and task executors on virtual threads
management.tracing.sampling.probability=1.0  # Micrometer Tracing: one trace per request in dev
@Service
class PricingService {
    private final RestClient rest;
    private final JdbcClient jdbc;

    PricingService(RestClient.Builder builder, JdbcClient jdbc) {
        this.rest = builder.baseUrl("https://rates.example.com").build();
        this.jdbc = jdbc;
    }

    @Observed(name = "pricing.quote")                       // metrics + trace span from one annotation
    Quote quote(String isin) {
        BigDecimal rate = rest.get().uri("/fx/CHF").retrieve().body(BigDecimal.class);   // blocking, on a virtual thread
        return jdbc.sql("select price from quotes where isin = :isin")
                   .param("isin", isin)
                   .query((rs, i) -> new Quote(isin, rs.getBigDecimal("price").multiply(rate)))
                   .single();
    }
}

Why it mattered. With virtual threads, the plain servlet stack scales like the reactive one while keeping stack traces, debuggers and the thread-per-request mental model. For a regulated system, that is the more auditable choice.

Spring Boot 4.0 and Spring Framework 7

November 2025 Spring Framework 7 · Java 17+, Java 25 recommended · Jakarta EE 11 overview ↑

Modular jars, null safety, API versioning, resilience in the core.

70+ focused modulesJSpecify null safetyJackson 3API versioningHTTP service clients@Retryable and @ConcurrencyLimitRestTestClientBeanRegistrarJakarta EE 11: Servlet 6.1, Validation 3.1, Persistence 3.2Spring Boot 4.1: gRPC, SSRF mitigation, OpenTelemetry updates

Boot 4.0 is the current generation. The codebase was split into focused modules so an application pulls only what it uses. JSpecify annotations across the portfolio give IDEs and Kotlin precise nullability. Jackson 3 is the JSON library. Spring Framework 7 brought three things teams had been building by hand: first-class API versioning in Spring MVC and WebFlux, declarative HTTP service clients with auto-configured groups, and resilience annotations (@Retryable, @ConcurrencyLimit) in the core rather than in a separate project. Spring Boot 4.1 (June 2026) added Spring gRPC support, an SSRF mitigation filter for HTTP clients and updated OpenTelemetry support; 4.2 is in milestones.

// API versioning: the same handler class, two contract versions
@RestController
@RequestMapping("/accounts")
class AccountController {
    @GetMapping(path = "/{id}", version = "1")
    AccountV1 v1(@PathVariable String id) { return service.v1(id); }

    @GetMapping(path = "/{id}", version = "2")                 // clients send X-API-Version: 2 or ?version=2
    AccountV2 v2(@PathVariable String id) { return service.v2(id); }
}
// HTTP service client: an interface, no implementation, auto-configured by Boot 4
@HttpExchange("/fx")
interface RatesClient {
    @GetExchange("/{currency}")
    BigDecimal rate(@PathVariable String currency);
}

@Service
class QuoteService {
    private final RatesClient rates;
    QuoteService(RatesClient rates) { this.rates = rates; }

    @Retryable(maxAttempts = 3, delay = 200)                   // Spring Framework 7 core resilience (@EnableResilientMethods)
    @ConcurrencyLimit(20)                                      // at most 20 concurrent calls into the rates API
    BigDecimal chf(String currency) { return rates.rate(currency); }
}
# application.properties, Spring Boot 4
spring.http.client.service.group.rates.base-url=https://rates.example.com
spring.http.client.service.group.rates.read-timeout=2s
spring.threads.virtual.enabled=true

Why it mattered. The migration cost of 4.0 is real (Jackson 3, module splits, removed deprecations) but it is the last big move for years: Framework 7 keeps Java 17 as the floor while recommending 25, so the runtime path is clear through the next two LTS releases.

Part two

What Spring AI can do now.

Spring AI 1.0 went GA in May 2025; 2.0 followed in June 2026 on the Boot 4 generation, and 2.0.1 is current. The idea is the one Spring applied to databases and messaging twenty years ago: a portable abstraction over the providers, with the cross-cutting concerns as reusable components. Eight capabilities, each with the code.

Providers and stores, Spring AI 2.0

Chat and embeddings: Anthropic, OpenAI, Amazon Bedrock, Google GenAI, Mistral AI, DeepSeek, Ollama, with vendor-maintained extensions for Azure and OCI. Images, audio and transcription where the provider offers them. Vector stores: pgvector, Redis, Elasticsearch, OpenSearch, MongoDB Atlas, Neo4j, Cassandra, Oracle, Pinecone, Qdrant, Weaviate, Milvus, Chroma and more. Memory: in-memory, JDBC, Cassandra, Neo4j. Observability: Micrometer observations for every model, tool and store call.

Talk to a model

ChatClient Spring AI ↑

ChatClient is the front door: a fluent API in the style of RestClient, built from an auto-configured builder for whichever provider is on the classpath. The same code runs against Anthropic, OpenAI, Amazon Bedrock, Google GenAI, Mistral, DeepSeek or a local Ollama model; the provider is a dependency and a property, not a code path.

<!-- pom.xml: Spring AI 2.0 BOM plus the Anthropic starter -->
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.ai</groupId>
      <artifactId>spring-ai-bom</artifactId>
      <version>2.0.1</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>
<dependency>
  <groupId>org.springframework.ai</groupId>
  <artifactId>spring-ai-starter-model-anthropic</artifactId>
</dependency>
# application.properties
spring.ai.anthropic.api-key=${ANTHROPIC_API_KEY}
spring.ai.anthropic.chat.model=claude-sonnet-5        # Spring AI 2.0 dropped the extra ".options" segment
@RestController
class AskController {
    private final ChatClient chat;

    AskController(ChatClient.Builder builder) {
        this.chat = builder
            .defaultSystem("You are the support assistant for a Swiss fasting app. Answer in the user's language, briefly.")
            .build();
    }

    @GetMapping("/ask")
    String ask(@RequestParam String q) {
        return chat.prompt().user(q).call().content();
    }

    @GetMapping(value = "/ask/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    Flux<String> stream(@RequestParam String q) {
        return chat.prompt().user(q).stream().content();      // tokens as they arrive
    }
}

Get typed answers back

Structured output Spring AI ↑

A model's text answer becomes a Java object with .entity(). Spring AI generates a JSON schema from the record, instructs the model to fill it, and maps the response. In 2.0 you can ask the provider to enforce the schema natively and validate the result; a StructuredOutputValidationAdvisor can also repair non-conforming JSON automatically.

record Triage(String category, int priority, List<String> nextSteps) {}

Triage triage = chat.prompt()
    .system("Classify support tickets for a banking app. Priority 1 is highest.")
    .user(ticketText)
    .call()
    .entity(Triage.class, spec -> spec
        .useProviderStructuredOutput()                        // the provider's native JSON schema mode
        .validateSchema());                                   // reject anything that does not match

Why it matters. Typed output is the boundary between a chat toy and a system component. Downstream code deals with a Triage, not with parsing prose, and a schema violation is an exception you can route to a dead-letter queue.

Let the model act

Tool calling and the agent loop Spring AI ↑

A @Tool method is a Java method the model may ask to run. Spring AI sends its name, description and parameter schema to the model, executes the call when the model requests it, feeds the result back, and repeats until the model answers. In 2.0 that loop is a ToolCallingAdvisor in the advisor chain, so it is one composable, observable component rather than logic buried in each provider. ToolSearchToolCallingAdvisor indexes a large tool set and lets the model discover only the tools it needs.

@Component
class AccountTools {
    private final AccountRepository accounts;
    AccountTools(AccountRepository accounts) { this.accounts = accounts; }

    @Tool(description = "Get the current balance of an account, in CHF")
    BigDecimal balance(@ToolParam(description = "IBAN of the account") String iban) {
        return accounts.balance(iban);
    }

    @Tool(description = "Block a card. Requires an explicit customer request. Returns the case number.")
    String blockCard(@ToolParam(description = "last four digits") String last4,
                     @ToolParam(description = "reason given by the customer") String reason) {
        return accounts.blockCard(last4, reason);            // the policy check lives here, not in the prompt
    }
}

@Service
class AssistantService {
    private final ChatClient chat;
    AssistantService(ChatClient.Builder builder, AccountTools tools) {
        this.chat = builder.defaultTools(tools).build();      // the tool loop is automatic in 2.0
    }
    String handle(String message) {
        return chat.prompt().user(message).call().content();
    }
}

Why it matters. This is the agent pattern from the AI blueprint in Spring form: every action is a typed capability with a description the model can read, and every capability is ordinary Java where authorisation, logging and rollback are enforced before anything happens.

Remember the conversation

Chat memory and advisors Spring AI ↑

Advisors are middleware for prompts: each one can read and rewrite the request and the response. Memory, retrieval, guardrails and logging are all advisors, which keeps business code free of them. The memory advisor stores turns per conversation ID in an in-memory, JDBC, Cassandra or Neo4j store, and the window implementation caps how many turns are replayed.

@Configuration
class ChatConfig {
    @Bean
    ChatClient supportChat(ChatClient.Builder builder, ChatMemory memory) {   // ChatMemory auto-configured, JDBC-backed with the starter
        return builder
            .defaultAdvisors(
                MessageChatMemoryAdvisor.builder(memory).build(),
                SafeGuardAdvisor.builder().sensitiveWords(List.of("password", "PIN")).build(),
                new SimpleLoggerAdvisor())
            .build();
    }
}

// per call: the conversation ID selects the memory
String answer = supportChat.prompt()
    .user(message)
    .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, sessionId))
    .call()
    .content();

Ground answers in your documents

Vector stores and RAG Spring AI ↑

Retrieval-augmented generation in Spring AI is an ETL pipeline plus one advisor. Readers (PDF, Tika for Office formats, JSON, Markdown) produce documents; a splitter chunks them; an embedding model turns chunks into vectors; a VectorStore stores them. Twenty stores are supported, from pgvector and Redis to Pinecone, Qdrant, Weaviate, MongoDB Atlas and Oracle. At query time QuestionAnswerAdvisor retrieves the best chunks and appends them to the prompt.

@Service
class PolicyIngest {
    private final VectorStore store;
    PolicyIngest(VectorStore store) { this.store = store; }         // pgvector via spring-ai-starter-vector-store-pgvector

    void ingest(Resource pdf) {
        List<Document> pages  = new PagePdfDocumentReader(pdf).get();
        List<Document> chunks = new TokenTextSplitter().apply(pages);
        store.add(chunks);                                           // embeds and stores in one call
    }
}

@Service
class PolicyAnswers {
    private final ChatClient chat;
    PolicyAnswers(ChatClient.Builder builder, VectorStore store) {
        this.chat = builder
            .defaultAdvisors(QuestionAnswerAdvisor.builder(store)
                .searchRequest(SearchRequest.builder().topK(4).similarityThreshold(0.7).build())
                .build())
            .build();
    }
    String answer(String question) {
        return chat.prompt().user(question).call().content();       // grounded in the retrieved chunks
    }
}

Why it matters. The retrieval step is where privacy lives. Documents never leave the vector store you run; only the retrieved chunks travel to the model, and a similarity threshold stops the model from being fed irrelevant context it might hallucinate around.

Expose and consume tools across processes

Model Context Protocol Spring AI ↑

Spring AI 2.0 ships the MCP Java SDK 2.0 and puts the annotation model in core. A Spring service becomes an MCP server by annotating methods with @McpTool, @McpResource or @McpPrompt; Streamable HTTP is the default transport, with OAuth 2.0 and OpenTelemetry supported out of the box. On the other side, an MCP client connects to any server (yours, a vendor's, or a desktop tool's) and its tools appear to the ChatClient like local ones.

// MCP server: spring-ai-starter-mcp-server-webmvc
@Component
class CalendarMcp {
    @McpTool(name = "free-slots", description = "Free 30-minute slots for a person on a given day")
    List<String> freeSlots(@McpToolParam(description = "email") String person,
                           @McpToolParam(description = "ISO date") LocalDate day) {
        return calendar.freeSlots(person, day);
    }
}
# MCP client: spring-ai-starter-mcp-client, connect to two servers
spring.ai.mcp.client.streamable-http.connections.calendar.url=http://calendar.internal:8080
spring.ai.mcp.client.streamable-http.connections.docs.url=http://docs.internal:8080
@Service
class Scheduler {
    private final ChatClient chat;
    Scheduler(ChatClient.Builder builder, ToolCallbackProvider mcpTools) {    // every tool from every connected server
        this.chat = builder.defaultToolCallbacks(mcpTools).build();
    }
    String plan(String request) { return chat.prompt().user(request).call().content(); }
}

Why it matters. MCP is how an agent reaches systems it does not own without bespoke integrations. The runnable MCP agent skeleton shows the same protocol from a Python client; the Spring version above is what it looks like inside a Boot service. For the whole protocol, and a complete verified server in Java, Spring AI and Python, read MCP end to end.

Prove it works, and keep watching

Evaluation and observability Spring AI ↑

Spring AI has evaluators that use a model to grade a model: RelevancyEvaluator checks that an answer is relevant to the question and the retrieved context, FactCheckingEvaluator checks claims against the context. They run in ordinary tests. At runtime, every ChatClient call, tool call, embedding and vector search emits Micrometer observations, so token usage, latency and error rates are on the same dashboards as the rest of the service.

@SpringBootTest
class PolicyAnswersTest {
    @Autowired ChatClient.Builder builder;
    @Autowired VectorStore store;
    @Autowired PolicyAnswers answers;

    @Test
    void answerIsRelevantAndGrounded() {
        String question = "How many days do I have to report a lost card?";
        String answer   = answers.answer(question);
        List<Document> context = store.similaritySearch(SearchRequest.builder().query(question).topK(4).build());

        var evaluator = new RelevancyEvaluator(builder);
        EvaluationResponse result = evaluator.evaluate(new EvaluationRequest(question, context, answer));

        assertThat(result.isPass()).as(result.getFeedback()).isTrue();
    }
}

Why it matters. An eval suite is the regression test of a prompt. Change the model, the system prompt or the chunking and the build tells you what got worse. The evals article covers how to design the suite.

Compose agents

Workflow patterns Spring AI ↑

Spring AI does not impose an agent framework; it gives you the primitives and documents the five patterns that cover most production agents: prompt chaining, routing, parallelisation, orchestrator-workers and evaluator-optimiser. Each is a few lines of ordinary Java over ChatClient, which means each is testable and debuggable like any other service method. The community spring-ai-agent-utils project adds reusable Agent Skills, and spring-ai-session adds event-sourced conversation memory with model-driven compaction.

// Evaluator-optimiser: draft, grade, revise, stop when good enough
record Grade(boolean pass, String feedback) {}

String draftAndRefine(String brief) {
    String draft = chat.prompt().user("Write a customer notice: " + brief).call().content();
    for (int round = 0; round < 3; round++) {
        Grade grade = chat.prompt()
            .system("You review customer notices for a Swiss bank. Pass only if it is accurate, calm and under 120 words.")
            .user("Brief: " + brief + "\n\nNotice:\n" + draft)
            .call().entity(Grade.class);
        if (grade.pass()) return draft;
        draft = chat.prompt().user("Revise this notice. Feedback: " + grade.feedback() + "\n\n" + draft).call().content();
    }
    throw new IllegalStateException("could not produce an acceptable notice");   // a human takes over
}

Why it matters. The loop has a bound, a typed exit condition and a failure path. That is the difference between an agent you can put behind a change-approval board and one you cannot.

Inside a regulated perimeter

The checklist before an AI feature ships.

Typed boundaries

Structured output in, @Tool methods out. Nothing downstream parses prose; nothing the model can call bypasses the service's own authorisation.

Data stays home

Vector store and chat memory run in your infrastructure. Only retrieved chunks and the current turn travel to the provider, and the provider is a contract you reviewed.

Evals in the build

Relevancy and fact-checking evaluators run as tests. A prompt change without a green eval suite does not merge.

Observed and bounded

Every call has a trace, a token count and a cost. Every agent loop has a maximum number of rounds and a human hand-off when it is reached.

See the practice applied.

The AI page collects the production patterns behind this checklist; the Java guide is the language side of the same story.