ADR-0012: Migrate core to Spring Boot 4.1 + Spring AI 2.0, and adopt selected 2.x features¶
| Status | Proposed. P1 (prep on Boot 3.5) is done; P2/P3 (Boot 4.1, Spring AI 2.0) are not started. Checked 2026-10-21 (v0.1.9): core/pom.xml is on Spring Boot 3.5.16 and Spring AI 1.1.8, and the P1b helpers named under Progress now exist (core/json/.../JsonMappers.java, core/chat/.../OpenAiCompatModelBuilder.java, core/chat/.../LlmRetry.java). |
| Progress | P1a done (2026-09-30): Spring Boot 3.4.4 to 3.5.16, Spring AI 1.1.7 to 1.1.8, testcontainers.version override dropped (Boot 3.5.16 manages 1.21.4), spring-ai-starter-model-* replaced by plain model modules in chat and eval (exclusion lists in CoreApplication, JobsApplication, WorkerApplication shrunk to the MCP ToolCallbackConverterAutoConfiguration), migrator Maven profile added, one property fix. Full reactor 6,551 tests, 0 failures; P0 guards unchanged. P1b (JsonMappers, OpenAiCompatModelBuilder, option helpers, LlmRetry) not started. Log: migration-p1a.md. |
| Date | 2026-09-30 |
| Deciders | Platform Engineering (maintainers) |
| Related | ADR-0003 Spring AI dynamic clients (docs/platform-documentation/decisions/), ADR-0011 provider plugins, docs/engineering/INTEGRATION-TESTS.md, docs/engineering/ERROR-PRONE-BASELINE.md |
Evidence rules for this document. Code facts come from grep over
core/**/pom.xmlandcore/*/srcon 2026-09-30 (commit1447cfd). Nothing was compiled or run. Version facts come from the sources in section 11, fetched on 2026-09-30. Where a source was ambiguous or I could not confirm something, the text says verify. Treat every "verify" as a task in phase P0/P1, not as a settled fact.
1. Context¶
1.1 Where we are¶
core/pom.xml: parentspring-boot-starter-parent3.4.4,java.version21,spring-ai.version1.1.7 (importedspring-ai-bom),testcontainers.version1.21.4. The reactor has about 45 modules.- Spring AI is used as a model-invocation and tool-calling substrate only. There is no
auto-configured
ChatModel. EveryChatClientis built per tenant and per provider bycore/chat/*AgentChatClientFactory(BYOK plus the hosted pool).CoreApplication,JobsApplicationandWorkerApplicationeach exclude 13 Spring AI auto-configurations (Anthropic, ChatClient, ChatMemory, 6 OpenAI, 3 Google GenAI, andToolCallbackConverterAutoConfiguration). - RAG (
core/knowledge: pgvector + full-text + RRF, tenant-scoped, sidecar embeddings), memory (core/agentsChatMemoryRecall,core/graphmemoryon Apache AGE), evals (core/quality,core/eval) and observability (LlmMetricsAdvisor,LlmCallLog, Langfuse over OTLP) are all hand-written. Spring AI'sVectorStore,EmbeddingModel,ChatMemoryandorg.springframework.ai.evaluationhave zero usages. (Correction to an earlier note:BeanOutputConverteris now used, once, inchat/structured/StructuredOutput.java.)
1.2 Where the ecosystem is (as of 2026-09-30)¶
| Component | Latest GA | Notes |
|---|---|---|
| Spring AI | 2.0.1 (2026-08-21). 2.0.0 GA was 2026-06-12. | "Spring AI 2.0.x supports Spring Boot 4.0.x and 4.1.x." 2.1.0-M1 (2026-09-25) moves the baseline to Boot 4.2 and is a milestone, so it is not a target. |
| Spring Boot | 4.1.1 (2026-08-21) | 4.1 OSS support runs to 2027-07-31. 4.0 OSS support ends 2026-12-31. |
| Spring Boot 3.5 / 3.4 | 3.5.16 / 3.4.13 | Both are past OSS end of life: 3.4 ended 2025-12-31 and 3.5 ended 2026-06-30. We run 3.4.4, which is behind even 3.4's final patch. |
| Spring Framework | 7.0.x (Boot 4.1 ships 7.0.8 per HeroDevs) | Jakarta EE 11, Servlet 6.1, JSpecify nullness. |
| Spring Security | 7.0.x | Lambda DSL only. AntPathRequestMatcher and MvcRequestMatcher are removed. Dispatcher types are authorized by default. |
| Hibernate ORM | 7.x (Boot 4.0 ships 7.2) | |
| Jackson | 3.0.x (tools.jackson.*). Jackson 2 is still supported but deprecated. |
Annotations stay in com.fasterxml.jackson.annotation. |
| Testcontainers | 2.0.x | Artifacts are renamed with a testcontainers- prefix. Packages move (for example org.testcontainers.postgresql.PostgreSQLContainer). JUnit 4 support is removed. |
| Flyway | 11.x | It needs spring-boot-starter-flyway under Boot 4's modular starters. |
| Micrometer / Tracing | 1.16 / 1.6 (Boot 4.0) | New spring-boot-starter-opentelemetry. |
| Kafka client / spring-kafka | 4.1 / 4.0 | |
| MCP Java SDK | 2.0.0 (via Spring AI 2.0) | Implements MCP spec 2025-11-25. The WebMVC and WebFlux transports moved into Spring AI (org.springframework.ai:mcp-spring-*). |
| Java | Boot 4.x minimum is 17. Boot 4.1 supports 17 to 26 and has first-class support for 25. | Nothing forces Java 21 → 25. |
2. Decision drivers¶
- Security and support. We are on an EOL line (3.4.4). 3.5 is also EOL for OSS. The only OSS-supported path is 4.x, and 4.1 has the longer window of the two.
- Spring AI 1.1 is a dead end for features. The 2.0 capabilities we want (tool-calling advisor, MCP SDK 2.0 / spec 2025-11-25, native cache-token usage, structured-output validation) are only available on Boot 4.
- The MCP spec moves both ways. We are both an MCP server (
core/mcpserver) and an MCP client (DynamicMcpConnectionManager). Staying on SDK 0.18.x drifts us away from the spec that Claude, ChatGPT and IDE clients now speak. - Blast-radius control. This is a multi-tenant production platform. The migration must never change the JSON API shape, JSONB column contents, auth behaviour or billing (token metering) without a test catching it.
- Keep what we built well. Tenant-isolated RAG with RRF, schema-per-tenant memory, and our own governance advisors are better than the framework defaults for this platform. We adopt framework features only where they remove code or add capability without weakening isolation.
3. Options considered¶
| # | Option | Verdict |
|---|---|---|
| A | Stay on Boot 3.4.4 / Spring AI 1.1.7 | Rejected. EOL and unpatched, and it gets no MCP spec updates. |
| B | Boot 3.5.x only (the last 3.x) | Rejected as an end state, because 3.5 is already OSS-EOL. It is accepted as a stepping stone (P1): 3.5 surfaces most Boot 4 deprecations as warnings, and Spring Security 6.5 has the 7.0 preparation flags. |
| C | Big-bang: Boot 4.1 + Jackson 3 + Spring AI 2.0 in one PR | Rejected. About 425 files touch Jackson, and 13 or more files touch removed Spring AI APIs. The tool loop also moves out of ChatModel. One PR could not be reviewed or bisected. |
| D | Phased: P1 prep on 3.5 → P2 platform (Boot 4.1 + Jackson 3) → P3 Spring AI 2.0 → P4 adopt features | Chosen. Each phase ships behind a green verification gate and can be reverted on its own. |
| E | Boot 4.2 / Spring AI 2.1 | Deferred. Both are milestones today. Revisit once 2.1 is GA; the 2.1 features we want are the Responses API, MessageParts and Session. |
4. Decision¶
Adopt option D. Targets are Spring Boot 4.1.x (4.1.1 today), Spring AI 2.0.x (2.0.1 today),
Java 21 (unchanged), Jackson 3 for our own code (with Jackson-2 wire defaults held on
explicitly), Testcontainers 2.0.x, and the MCP Java SDK 2.0.x managed by the Spring AI BOM.
The work happens on a dedicated branch (feat/spring-ai-2-boot4) and merges only after the
section 7 gates pass on staging.
Two structural sub-decisions:
- Drop the Spring AI model starters in favour of the plain model artifacts. Use
spring-ai-openai,spring-ai-anthropicandspring-ai-google-genaiincore/chatandcore/eval, and keepspring-ai-client-chateverywhere else, as 7 modules already do. We never use the auto-configured beans. This removes most of the 13-class exclusion list, and with it the "a Spring AI bump silently adds a new auto-config" failure mode. This is done in P1. - One seam per concern. Before P3, the 9 OpenAI-compatible factories must go through a single
OpenAiCompatModelBuilder, and all JSON must go through a singleJsonMappersfactory. That way the Spring AI 2 changes and the Jackson 3 changes each land in one place, not in dozens.
5. Consequences¶
Positive
- We are back on an OSS-supported line until mid-2027, with a clear path to 4.2 / Spring AI 2.1.
- The MCP client and server on spec 2025-11-25 gain Streamable HTTP as the default, stateless
servers, structured tool output, elicitation, and OAuth 2.0 support modules.
- Per-provider cache-token parsing (CacheTokenAccounting) can be replaced by
Usage#getCacheReadInputTokens and getCacheWriteInputTokens.
- The OpenAI and Anthropic clients move to the vendors' official Java SDKs, so we no longer
maintain drift in hand-rolled request records.
Negative / costs
- The tool loop moves out of ChatModel into ToolCallingAdvisor. Our resilience wrappers
(ResilientToolCallingManagerFactory, ArgumentSanitizingToolCallingManager,
RedactingToolExecutionExceptionProcessor) and two custom models (GeminiMixedContentToolCallChatModel,
AwsBedrockChatModel) must be re-seated. Advisor ordering also changes what LlmMetricsAdvisor
and the budget advisors see, so billing/metering is at risk (section 8).
- OpenAiApi and AnthropicApi are removed. Our HTTP-client tuning (LlmHttpClientFactory:
Apache HC5 pool, JDK streaming connector, first-byte timeouts) no longer applies to OpenAI and
Anthropic, because those modules are now on OkHttp inside the vendor SDKs. There is no
completionsPath option, and the hosted pool relies on it.
- Jackson 3 is a mechanical change to about 425 files, with real behaviour differences (dates,
property ordering, checked → unchecked exceptions, renamed JsonNode methods).
- MCP SDK 2.0 removes McpSyncHttpClientRequestCustomizer, which our per-request OAuth header
refresh (McpRemoteHeaders) is built on.
- Langfuse and Grafana queries keyed on Spring AI span names must change (tool_call <name> →
execute_tool <name>; gen_ai.operation.name framework → execute_tool).
6. Migration plan¶
Effort is in engineer-days for one engineer who knows the codebase, and includes writing tests. File counts come from grep (section 10).
P0: Characterisation tests (on current 3.4.4), about 4 to 6 days¶
Without these, the P2 and P3 gates cannot detect regressions. All are additive tests.
- API JSON golden tests. Serialise a representative DTO set (errors, agent, chat turn, usage,
workflow run) and compare it against checked-in JSON. Cover date formats, property order,
nulls and empty collections. This extends
ErrorResponseContractTest. - JSONB round-trip tests. Cover each of the 12
@JdbcTypeCode(SqlTypes.JSON)entities plus the JdbcTemplate JSON writers. Read a fixture row written by the current code, then write it back and compare. Run this in theitprofile againstpgvectorPostgres. - Tool-loop behaviour tests against a scripted fake
ChatModelthat emits tool calls: argument sanitisation, redaction of tool exceptions, the max-iteration guard,ToolContexttenant propagation,returnDirect, and cancellation. - Metering tests. The prompt/completion/cache token totals that
LlmMetricsAdvisor,FreeTierTokenTrackingAdvisorandPerUserBudgetTrackingAdvisorrecord for a 3-iteration tool turn, streaming and non-streaming. This is the most important P0 test, because P3 changes where usage accumulates. - MCP contract tests: the server
POST /mcpStreamable HTTP handshake plus a tools/list and tools/call againstMcpServerToolConfig, and a client test that asserts the OAuth bearer is re-read per request (theMcpRemoteHeadersinvariant).
P1: Prepare on Boot 3.5.x (last 3.x), about 6 to 9 days¶
The goal is to clear deprecations and create the seams, with no behaviour change.
| Change | Mechanical? | Files (est.) |
|---|---|---|
Parent 3.4.4 → 3.5.16. Remove the testcontainers.version override if 3.5 manages ≥1.21.4 (verify). |
mech | 1 |
| Spring AI 1.1.7 → latest 1.1.x patch (verify whether one exists after 1.1.7). | mech | 1 |
Swap spring-ai-starter-model-* → plain spring-ai-openai / -anthropic / -google-genai in chat and eval, and shrink the 3 exclusion lists to what is still on the classpath (the MCP client/server starters stay). |
manual | 5 |
Run OpenRewrite org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_5 and Spring Security 6.5 → 7 prep recipes. Turn on the Security 6.5 opt-in 7.0 defaults. |
mech | 3 security configs |
Introduce JsonMappers (one configured ObjectMapper/JsonMapper factory) and replace the 179 main-code new ObjectMapper() sites with it. This is the single biggest de-risking step for Jackson 3. |
semi-mech | ~180 |
Introduce OpenAiCompatModelBuilder and route all 9 OpenAI-compatible factories plus HostedLlmChatClientFactory through it. Do the same for Anthropic (AnthropicAgentChatClientFactory, EvalRunner). |
manual | ~13 |
Move options.copy() (4 sites: ArgumentSanitizingToolCallingManager, StructuredOutput) and ModelOptionsUtils.OBJECT_MAPPER (GeminiMixedContentToolCallChatModel, GeminiSchemaSelfHealer) behind small helpers. |
manual | 4 |
Wrap the spring-retry usage (LlmModelSupport, GeminiMixedContentToolCallChatModel) behind an internal LlmRetry interface. |
manual | 3 |
Add spring-boot-properties-migrator (runtime scope, removed in P2) and fix every warning. |
manual | yml |
Gate P1: the gate set in section 7, plus zero properties-migrator warnings. Rollback: revert the P1 PR. There are no data or format changes.
P2 + P3: Boot 4.1 platform and Spring AI 2.0, on one branch¶
P2 cannot ship on its own, because Spring AI 1.1 does not run on Boot 4.
Spring AI 1.1 cannot load on Boot 4 ("built on the Boot 4.0 dependency model and cannot be loaded in a 3.x context", and the reverse is equally true). P2 and P3 therefore land on the same branch. They are still reviewed as separate commits (P2 commit set, then P3 commit set), so a bisect can tell a platform regression from a Spring AI regression.
P2 commits: platform, about 10 to 15 days¶
| Change | Tooling | Files (est.) |
|---|---|---|
Parent → 4.1.1. Run org.openrewrite.java.spring.boot4.UpgradeSpringBoot_4_0 (community edition in rewrite-spring). It chains Boot 3.5, Framework 7, Security 7, Hibernate 7.1 and Testcontainers2Migration. |
OpenRewrite | all poms |
Starter renames: spring-boot-starter-web → spring-boot-starter-webmvc (35 poms), add spring-boot-starter-flyway, and use per-technology test starters where needed. spring-boot-starter-classic is an acceptable interim. |
OpenRewrite + manual | ~45 poms |
Jackson 3: run org.openrewrite.java.jackson.UpgradeJackson_2_3 (package move com.fasterxml.jackson.{core,databind} → tools.jackson.*, JsonNode#asText → asString (656 calls in 140 files), fields() → properties(), isTextual() → isString(), JsonProcessingException → JacksonException (unchecked, 37 files), removal of JavaTimeModule/Jdk8Module registrations (9 files)). Annotations (26 files) are unchanged. |
OpenRewrite, then manual fix-ups | ~425 files (325 main) |
Hold Jackson-2 wire behaviour: spring.jackson.use-jackson2-defaults=true (dates as timestamps, no alphabetical sort, and so on), and make JsonMappers build with configureForJackson2()-equivalent features. Verify against the P0 golden files. |
manual | 2 |
Hibernate 7 JSON: pin the hibernate.type.json_format_mapper to a mapper built from JsonMappers, so JSONB output does not change silently. Verify which Jackson (2 or 3) Hibernate 7.x auto-detects when both are on the classpath: Temporal SDK, networknt json-schema-validator 2.0.0 and Playwright keep Jackson 2 on the classpath transitively. Re-check MultiTenantConnectionProvider / CurrentTenantIdentifierResolver generics on the schema-per-tenant SPI. |
manual | ~5 |
Security 7: 3 SecurityFilterChain classes. They already use the lambda DSL, with no .and() and no Ant/Mvc matchers. Main risk: dispatcher types are now authorized, so /error forwards can turn into 401s. Add dispatcherTypeMatchers(FORWARD, ERROR).permitAll() or equivalent. |
manual | 3 |
META-INF/spring.factories in app-config registers 3 EnvironmentPostProcessors. The interface moved to org.springframework.boot.EnvironmentPostProcessor (the old one is deprecated). Update the key and the imports. |
manual | 4 |
Testcontainers 2: 4 test files. Rename the artifacts (testcontainers-postgresql, testcontainers-junit-jupiter) and move to the new PostgreSQLContainer package. |
OpenRewrite | 5 |
Observability properties: management.otlp.tracing.* and the tracing starters moved in Boot 4 (verify the exact new keys with properties-migrator). Consider spring-boot-starter-opentelemetry. |
manual | yml |
Kafka 4.x client (spring-kafka, 3 main files), Lettuce (47 Redis files, API stable), 16 WebSocket files, HttpStatus deprecated constants (10 files). |
mostly mech | ~20 |
Not affected: no javax.* EE imports, no RestTemplate, no @MockBean/@SpyBean, no springdoc, no Spring Cloud, no Keycloak adapter (we use resource-server JWT), and Temporal via the plain temporal-sdk 1.31.0 rather than the Boot starter. Keep the SDK; bump it only if its Jackson 2 conflicts. |
none | 0 |
P3 commits: Spring AI 1.1.7 → 2.0.1, about 12 to 18 days¶
| # | Breaking change (upgrade notes) | Our impact | Files |
|---|---|---|---|
| 1 | Tool execution loop removed from every ChatModel; ChatClient auto-registers ToolCallingAdvisor (HIGHEST_PRECEDENCE + 300). |
Re-seat ResilientToolCallingManagerFactory and ArgumentSanitizingToolCallingManager on ToolCallingAdvisor.builder().toolCallingManager(...) inside LlmModelSupport.chatClientBuilder. Redesign GeminiMixedContentToolCallChatModel: it overrides a protected method of the model's internal loop, and that method no longer exists. Redesign AwsBedrockChatModel: it has its own ToolCallingChatOptions branch. Re-order our advisors explicitly around the tool advisor. |
~10 |
| 2 | ToolCallingAdvisor accumulates usage across all internal calls. Default limits are 40 per tool and 150 total (ToolCallLimitExceededException). |
Decide whether metering advisors sit outside the tool advisor (they see the accumulated total once) or inside (they see each iteration). Never both. Set limits explicitly to match our own guard. The P0 metering test is the gate. | 4 advisors |
| 3 | OpenAiApi removed. OpenAI is now on the official openai-java SDK (OkHttp). OpenAiChatModel.builder() takes options, httpClientBuilderCustomizer and dispatcherExecutor. Strict tool schemas now default to false. |
13 files, all behind the P1 seam. Port LlmHttpClientFactory timeouts and pool sizing to OpenAiHttpClientBuilderCustomizer. completionsPath has no equivalent: hosted-pool rows that set it need either an OkHttp interceptor that rewrites the path, or a base URL chosen so that {base}/chat/completions resolves. Verify for each hosted row. StructuredOutput ResponseFormat moves to the SDK's type. Decide per provider whether to opt in to strict(true) (our SchemaTypeArrayNormalizer exists because of strict-mode 400s). |
13 |
| 4 | AnthropicApi removed. Anthropic is on the official anthropic-java SDK. AnthropicChatModel.builder().apiKey(..). The maxTokens default goes 500 → 4096. The cache classes move from anthropic.api to org.springframework.ai.anthropic. |
AnthropicAgentChatClientFactory, EvalRunner, AnthropicProviderCachingStrategy, CacheTokenAccounting. |
4 |
| 5 | Options are immutable: copy()/fromOptions() are removed in favour of mutate(). ChatClient.options() takes a builder. N() → n(). |
4 sites (behind P1 helpers). There are no ChatClient.options(built) calls today. |
2 |
| 6 | ModelOptionsUtils JSON members removed; use JsonHelper / JacksonUtils.getDefaultJsonMapper(). |
Gemini schema parsing (GeminiSchemaSelfHealer, GeminiMixedContentToolCallChatModel). |
2 |
| 7 | retryTemplate(...) now takes org.springframework.core.retry.RetryTemplate (Framework 7), not spring-retry. |
LlmModelSupport (its RetryListener metrics must be ported), Gemini model. |
3 |
| 8 | MCP SDK 2.0: McpSyncClientCustomizer → McpClientCustomizer<McpClient.SyncSpec>; McpSyncHttpClientRequestCustomizer removed (use httpRequestCustomizer on the transport builder); McpSchema.Tool.inputSchema() is now a Map; sealed interfaces removed; required-field builders (CallToolRequest.builder(name), and so on). |
McpClientTimeoutConfig, McpRemoteHeaders (per-request OAuth: re-prove it with the P0 test), DynamicMcpConnectionManager, StaticMcpStdioSidecarRouting (NamedClientMcpTransport), McpServerToolConfig (McpServerFeatures, McpToolUtils). |
~7 |
| 9 | Tool-resolution fallback is disabled by default. | We always attach explicit callbacks, so no impact is expected. Verify AgentToolProvider. |
0–1 |
| 10 | Auto-config class names and packages changed or were added (for example tool-search and tool-calling auto-configs). | Re-derive the exclusion lists from the 2.0.1 jars. After the P1 starter swap, only the MCP ones should remain. | 3 |
| 11 | Observation names: execute_tool <name>, gen_ai.operation.name=execute_tool, new spring.ai.tool.type and spring.ai.tool.call.id. |
Langfuse views, alert rules, TracingSamplingDefaultTest-style assertions. |
dashboards |
| 12 | mcp-json-jackson2 → Jackson 3 JSON mapper in SDK 2.0 (verify). |
The agents/pom.xml comment already warns that json-schema-validator arrived transitively through mcp-json-jackson2. Keep it declared explicitly at 2.0.0. |
1 |
Gate P2+P3: the full section 7 set, plus a staging soak (section 7.3).
Rollback: the branch is not merged into main until the soak passes. After merge and deploy,
roll back by redeploying the previous core image tag. P2/P3 add no Flyway migrations, so the
image rollback is clean. If P4 adopts JDBC chat memory, its migration is additive only.
P4: Adopt selected Spring AI 2.x features, incremental, about 10 to 20 days in total¶
Each item is a separate PR on Boot 4 main, flag-gated, and follows section 9.
7. Verification gates (every phase)¶
7.1 Build gates (JDK 21 toolchain; one Maven build at a time)¶
mvn -f core/pom.xml clean verify: the full reactor, unit tests, ArchUnit (core/app/src/test/.../archrules), Error Prone + NullAway (-Pstatic-analysis). Expect new NullAway findings from Framework 7's JSpecify annotations in@NullMarkedpackages. Fix them rather than baselining them.mvn -f core/pom.xml -Pit verify: TestcontainersApplicationContextIT(full context boot),MigrationsIT(all Flyway versions, controlplane and tenant),KnowledgeVectorSearchIT(pgvector + RRF).- Boot the actual jar (
java -jar core/app/target/*.jar) againstdeploy-postgres:latest(pgvector + AGE).mvn testhas missed context-start failures before. - The P0 suites: API JSON golden files, JSONB round-trip, tool-loop, metering, MCP contract.
ErrorResponseContractTest, and the web e2e API contract (.github/workflows/e2e.yml).
7.2 Behavioural checks (manual, staging)¶
- One chat turn per provider family: OpenAI, Anthropic, Gemini (API + Vertex), Bedrock, an
OpenAI-compatible provider (Groq/DeepSeek/Mistral/Ollama), and each hosted-pool row that has a
custom
completionsPath. Test streaming and non-streaming, and with and without tools. - An MCP client against one remote Streamable HTTP server with an OAuth token that expires during
the session. An MCP server driven by an external client through
POST /mcpwith a grant key. - Langfuse shows
chat+execute_toolspans, and there is no prompt/completion text on spans (log-prompt/log-completionmust stay false; re-check the property names in 2.0).
7.3 Soak¶
48 hours on staging with synthetic traffic. Compare token totals per tenant against the P1 baseline (tolerance ±1%), p95 TTFT, error rate, and the thread and connection-pool counts that OkHttp now owns.
8. Risk register¶
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Token metering double-counts or under-counts after the tool loop moves into an advisor | High | High (billing, free-tier limits) | P0 metering test; explicit advisor order; soak comparison |
| Jackson 3 changes API JSON (dates, order, nulls) | High | High (web, mobile, SDK clients) | use-jackson2-defaults; JsonMappers; golden files |
| JSONB written in a new shape becomes unreadable by old code (during rollback) | Medium | High | Pin the Hibernate format mapper; round-trip tests; no mixed-version writers |
Hosted pool completionsPath cannot be expressed |
Medium | Medium | OkHttp path-rewrite interceptor; per-row test |
| HTTP timeouts regress (OkHttp defaults vs our tuned HC5/JDK clients) | Medium | High (hung turns) | Port LlmHttpClientFactory settings to OpenAiHttpClientBuilderCustomizer and its Anthropic equivalent; streaming first-byte timeout test |
| MCP remote OAuth header frozen at build time (a regression of a bug we already fixed once) | Medium | High | P0 per-request header test |
Security 7 authorizes /error, so 401s replace structured errors |
Medium | Medium | Dispatcher-type rule; ErrorResponseContractTest |
Hibernate 7 multi-tenant SPI / ddl-auto: validate gets stricter |
Low–Med | High (boot failure) | ApplicationContextIT + boot-the-jar gate |
| Two Jackson majors on the classpath (Temporal, networknt, Playwright pull Jackson 2) | Certain | Low | Fine by design (different packages). Just never inject a Jackson 2 ObjectMapper bean |
| OpenRewrite churn makes review impossible | Medium | Medium | Recipe-only commits separate from manual commits; spotless ratchet |
9. Feature adoption table (Spring AI 2.x)¶
"Fit" compares each feature against what we already have. Effort is engineer-days after P3.
| Feature | Benefit | Fit with our code | Risk | Effort | Recommendation |
|---|---|---|---|---|---|
ToolCallingAdvisor (unified loop, limits, ToolExecutionEligibilityChecker) |
One loop for every provider, and streaming parity | Mandatory in P3. It replaces the per-model loop our wrappers hook into | Metering order | (in P3) | Adopt (forced) |
Native cache-token usage (Usage#getCacheRead/WriteInputTokens) |
Deletes provider-specific parsing | Replaces most of CacheTokenAccounting |
Low | 1–2 | Adopt |
MCP SDK 2.0 / spec 2025-11-25: structured tool output (outputSchema), elicitation, progress, tool annotations |
Our MCP server becomes modern for Claude/ChatGPT/IDE clients, and the client can consume structured results | McpServerToolConfig builds specs programmatically per grant. Keep that approach, but emit outputSchema/annotations from our ToolDefinition metadata |
Medium | 3–5 | Adopt |
| Stateless Streamable HTTP MCP server | Lets mcpserver scale horizontally without sticky sessions (we run one replica today) |
Good fit: our tools are request-scoped and auth is by API key | Low | 1–2 | Adopt (config flip plus tests) |
@McpTool / @McpResource / @McpPrompt annotations |
Less boilerplate for static tools | Poor fit. Our exposed tools are dynamic, per tenant and per grant, and rate-limited | Low | – | Skip for tenant tools. Possibly use for a few static platform tools |
| MCP Security (OAuth 2.0 resource server / authorization server modules) | Spec-compliant OAuth for remote MCP clients | Complements our API-key grants and Keycloak | Medium (it is a community module; check its Boot 4 version) | 4–6 | Evaluate in P4 |
ToolSearchToolCallingAdvisor (regex/lucene/vector tool index) |
Scales to hundreds of MCP tools with progressive disclosure | Overlaps our ToolRelevanceSelector |
Medium | 3–5 | Evaluate with an A/B on the quality/stats harness; keep ours if it is not better |
StructuredOutputValidationAdvisor + BeanOutputConverter via JsonSchemaGenerator |
Automatic re-prompt on invalid JSON | Our StructuredOutput (13 callers) already does provider-native JSON mode |
Low | 2 | Adopt as the retry layer under StructuredOutput |
Anthropic options: thinking display, service tier, AnthropicWebSearchTool, inferenceGeo |
Data-residency (eu), cost control, server-side web search |
Map onto existing agent settings | Low | 2–3 | Adopt inferenceGeo + service tier. Web search is optional |
OpenAI extraBody, promptCacheKey |
Cleaner OpenAI-compatible extras; better cache hits | Replaces bespoke request tweaks | Low | 1–2 | Adopt |
Reasoning content (reasoningContent metadata; 2.1 ReasoningPart) |
Shows the thinking trace in the UI | Partial support in 2.0 (DeepSeek/vLLM only for OpenAI-compatible providers) | Low | 2 | Adopt read-only; wait for 2.1 MessageParts for full support |
JDBC ChatMemoryRepository (Postgres, sequence_id) |
Standard storage | Schema-per-tenant works via our datasource routing, but our memory recall is semantic plus graph (AGE) and richer | Medium (a second store for the same data) | 3 | Skip. Keep ChatMemoryRecall + graphmemory |
spring-ai-session (community; event-sourced memory + summarisation) |
Long-conversation compaction | Interesting. It becomes part of core in 2.1 | Medium | – | Revisit at 2.1 GA |
PgVector VectorStore + QuestionAnswerAdvisor / RetrievalAugmentationAdvisor |
Standard RAG API | Our RAG is better for us: hybrid FTS + vector with RRF, tenant schema isolation, HNSW tuning, re-embed jobs, sidecar embeddings | High (isolation regressions) | – | Skip. Optionally expose our search as a DocumentRetriever adapter later |
| ETL readers (PDF with page ranges, Tika) | Fewer custom parsers | Ingestion lives in the sidecar | Low | – | Skip (sidecar owns it) |
Evaluators (relevancy, fact-checking) |
Standard judge prompts | core/quality has a stats-aware engine (k-repeats, Wilson intervals, paired A/B) |
Low | 1 | Skip, or wrap as one extra Evaluator impl |
Observability (gen_ai.* semconv on chat and tool spans, tool-call ids) |
Better Langfuse traces without custom code | Complements LlmMetricsAdvisor; keep our DB LlmCallLog |
Low | 1–2 | Adopt (remap dashboards) |
| JSpecify null-safety across Spring AI | Pairs with our NullAway pilot | Good | Low | incl. | Adopt (automatic) |
Tool call limits (spring.ai.tools.limits.*) |
A runaway-loop guard | Set explicitly on the builder | Low | incl. | Adopt |
OpenAI Responses API, MessageParts, VectorStore.upsert(EmbeddedDocument) |
Reasoning + tools on current OpenAI models | Only in 2.1.0-M1 | – | – | Wait for 2.1 GA (needs Boot 4.2) |
| A2A | – | No first-party A2A in Spring AI 2.0 (none found); our core/agents/a2a stays |
– | – | Keep ours |
Agent frameworks (Spring AI agentic project planned Nov 2026, experimental; spring-ai-agent-utils, Embabel) |
– | Our agent runtime (AgentChatService, crews, workflows, Temporal) is the product |
– | – | Watch, do not adopt |
10. Appendix: code inventory (grep, 2026-09-30, commit 1447cfd)¶
10.1 Spring AI artifacts (versions from spring-ai-bom 1.1.7)¶
| Artifact | Modules |
|---|---|
spring-ai-client-chat |
agents, codeintel, graphmemory, guardrails, knowledge, quality, rl (7) |
spring-ai-starter-model-openai |
chat, eval |
spring-ai-starter-model-anthropic |
chat, eval |
spring-ai-starter-model-google-genai |
chat |
spring-ai-starter-mcp-client |
agents |
spring-ai-starter-mcp-server-webmvc |
mcpserver |
Pinned non-Spring libraries worth tracking: okhttp 4.12.0 (pinned in the root for google-genai),
temporal-sdk 1.31.0, json-schema-validator 2.0.0, playwright 1.49.0, bcprov-jdk18on 1.80,
AWS SDK 2.28.29, Lombok 1.18.38, ArchUnit 1.4.1.
10.2 Spring AI API usage¶
207 main and 72 test files import org.springframework.ai.*. Top classes by import count (main + test):
ToolCallback 134, ChatClient 111, ToolDefinition 105, Prompt 23, ChatResponse 23,
OpenAiChatOptions 21, AssistantMessage 18, OpenAiApi 16, ToolContext 16, ChatClientRequest 15,
GoogleGenAiChatOptions 13, Message 13, CallAdvisorChain 13, ChatClientResponse 13,
OpenAiChatModel 12, Generation 12, StreamAdvisorChain 11, UserMessage 10, Usage 8,
ToolCallingManager 7, DefaultToolDefinition 6, CallAdvisor 6, StreamAdvisor 6, AnthropicApi 5,
AnthropicChatOptions 5, ToolCallingChatOptions 5, ToolCallbackProvider 4, GoogleGenAiChatModel 4,
SyncMcpToolCallbackProvider 2, GoogleGenAiCachedContentService 2, ModelOptionsUtils 2,
BeanOutputConverter 1, McpToolUtils 1, McpSyncClientCustomizer 1, AnthropicCacheOptions 1.
MCP SDK direct imports: McpSyncClient 8, ServerParameters 6, StdioClientTransport 2,
HttpClientStreamableHttpTransport 1, HttpClientSseClientTransport 1,
McpSyncHttpClientRequestCustomizer 1, McpServerFeatures 1, JacksonMcpJsonMapper 1.
Hot files for P3: chat/ {Hosted,AzureOpenAI,DeepSeek,Grok,Groq,Meta,Mistral,Ollama,OpenAI,OpenRouter,Sarvam,Anthropic,Gemini,VertexAi}*ChatClientFactory,
LlmModelSupport, LlmHttpClientFactory, ResilientToolCallingManagerFactory, ArgumentSanitizingToolCallingManager,
RedactingToolExecutionExceptionProcessor, GeminiMixedContentToolCallChatModel, GeminiSchemaSelfHealer,
AwsBedrockChatModel, CacheTokenAccounting, AnthropicProviderCachingStrategy, GoogleGenAiCacheManager,
structured/StructuredOutput, LlmMetricsAdvisor, SystemMessageMergingAdvisor, DefaultAgentAdvisor,
gateway/GatewayGovernanceAdvisor; agents/ FreeTierTokenTrackingAdvisor, PerUserBudgetTrackingAdvisor,
tools/AgentToolProvider, tools/DynamicMcpConnectionManager, tools/McpRemoteHeaders, tools/McpClientTimeoutConfig,
tools/StaticMcpStdioSidecarRouting, tools/AgentMcpToolResolver; mcpserver/McpServerToolConfig; eval/EvalRunner;
app/CoreApplication, app-jobs/JobsApplication, app-worker/WorkerApplication (exclusions).
Custom classes that wrap or replace Spring AI features: tool-loop resilience (ResilientToolCallingManagerFactory,
ArgumentSanitizingToolCallingManager, RedactingToolExecutionExceptionProcessor, GuardedToolCallback,
NamespacedMcpToolCallback, SanitizedMcpToolCallback, SchemaTypeArrayNormalizer, ToolRelevanceSelector);
structured output (StructuredOutput + 13 callers); caching (ProviderCachingRegistry + 6 strategies);
RAG (KnowledgeSearchService, RRF); memory (ChatMemoryRecall, graphmemory); evals (quality/EvalEngine, eval/EvalRunner);
metrics (LlmMetricsAdvisor, LlmCallLog*); Bedrock (AwsBedrockChatModel, a hand-rolled ChatModel).
10.3 Boot-4-sensitive usages (files: total / main)¶
| Item | Count |
|---|---|
com.fasterxml.jackson.* imports |
424 / 325 |
ObjectMapper |
387 / 286; new ObjectMapper() 278 / 179 |
JsonNode |
182 / 143; .asText( 656 calls in 140 files; .isTextual() 36/24; .fields() 20/12 |
JsonProcessingException |
39 / 37 |
TypeReference< |
40 occurrences / 26 files |
JavaTimeModule/Jdk8Module |
9 / 2; registerModule( 8 files |
MappingJackson2HttpMessageConverter |
2 / 1 |
| Jackson annotations (unchanged in 3) | 26 / 26 |
javax.{persistence,servlet,validation,annotation,transaction,inject} |
0 |
RestTemplate |
0; RestClient 107/71; WebClient 18/18; java.net.http.HttpClient 48/42 |
SecurityFilterChain |
3 main (SecurityConfig, NoSecurityConfig, McpServerToolConfig); .and() 0; Ant/Mvc matchers 0; JwtDecoder 4 |
| Hibernate APIs | @JdbcTypeCode(SqlTypes.JSON) 12 files; MultiTenantConnectionProvider, CurrentTenantIdentifierResolver, MetadataSources (1 each); AttributeConverter 1; no UserType, @Type or @Where |
JdbcTemplate |
49 / 33 |
| Flyway API | 5 / 1 (no customizers) |
| Testcontainers | 4 test files (PostgreSQLContainer, GenericContainer, ImageFromDockerfile, @Testcontainers); @ServiceConnection 1 |
@MockBean/@SpyBean |
0; MockMvc 17 test files; TestRestTemplate 1 |
| spring-retry | 3 files (LlmModelSupport, GeminiMixedContentToolCallChatModel, a test) |
spring.factories |
1 (app-config, 3 EnvironmentPostProcessors); no AutoConfiguration.imports |
Redis (RedisTemplate/Lettuce) |
77 / 47; no Jackson Redis serializers |
| Kafka | 4 / 3; no Kafka JSON serde |
| Actuator custom endpoints/health | 3 |
Micrometer ObservationRegistry/@Observed |
20 / 18; micrometer-tracing-bridge-otel + opentelemetry-exporter-otlp in 3 app poms; OTel API 3 files |
| Temporal | 7 / 6 (plain temporal-sdk, no Boot starter) |
| WebSocket | 19 / 16 |
| springdoc / Spring Cloud / Keycloak adapters | none |
Deprecated HttpStatus names (PAYLOAD_TOO_LARGE/UNPROCESSABLE_ENTITY, and so on) |
14 / 10 |
11. Sources¶
- Spring AI 2.0.0 GA announcement, 2026-06-12: https://spring.io/blog/2026/06/12/spring-ai-2-0-0-GA-available-now/
- Spring AI 2.0.1, 2026-08-21: https://spring.io/blog/2026/08/21/spring-ai-2-0-1-available-now/
- Spring AI 2.1.0-M1, 2026-09-25 (Boot 4.2 baseline, Responses API, MessageParts, agentic project Nov 2026): https://spring.io/blog/2026/09/25/spring-ai-2-1-0-M1-available-now/
- Spring AI upgrade notes (2.0.0 breaking changes, OpenRewrite MCP recipes): https://docs.spring.io/spring-ai/reference/upgrade-notes.html
- Spring AI getting started ("2.0.x supports Spring Boot 4.0.x and 4.1.x"): https://docs.spring.io/spring-ai/reference/getting-started.html
- Spring AI OpenAI chat (2.0, SDK-based builder, no
completionsPath,OpenAiHttpClientBuilderCustomizer): https://docs.spring.io/spring-ai/reference/api/chat/openai-chat.html - Spring AI MCP overview / server starter / security: https://docs.spring.io/spring-ai/reference/api/mcp/mcp-overview.html, https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html, https://docs.spring.io/spring-ai/reference/api/mcp/mcp-security.html, https://github.com/spring-ai-community/mcp-security
- Stateless MCP on Spring AI 2.0 (community write-up): https://dev.to/jamilxt/mcp-goes-stateless-building-ai-agent-servers-with-spring-ai-20-4hfg
- Spring Boot 4.0 migration guide: https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Migration-Guide
- Spring Boot 4.0 release notes (dependency versions): https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Release-Notes
- Spring Boot support dates / Java ranges: https://endoflife.date/spring-boot ; https://www.herodevs.com/blog-posts/spring-boot-versions-eol-dates-and-latest-releases-april-2026
- Jackson 3 in Spring: https://spring.io/blog/2025/10/07/introducing-jackson-3-support-in-spring/ ;
use-jackson2-defaults: https://github.com/spring-projects/spring-boot/issues/49951 - Spring Security 7 migration: https://docs.spring.io/spring-security/reference/migration/index.html
- OpenRewrite recipes: https://docs.openrewrite.org/recipes/java/spring/boot4/upgradespringboot_4_0-community-edition ; https://docs.openrewrite.org/recipes/java/jackson/upgradejackson_2_3 ; https://docs.openrewrite.org/recipes/java/testing/testcontainers/testcontainers2migration ; https://docs.openrewrite.org/recipes/java/spring/security7/upgradespringsecurity_7_0
- Testcontainers 2 artifact/package changes: https://mvnrepository.com/artifact/org.testcontainers/testcontainers-postgresql ; https://blog.doubleslash.de/en/software-technologien/coding-and-frameworks/testcontainers-2-an-upgrade-worth-it/
Source caveats. One secondary source dates the 2.0 GA to May 28; spring.io's own post is dated 2026-06-12, and that is the date used here. The upgrade-notes summary I fetched described the new OpenAI module as built on "anthropic-java", which is plainly a summarisation error. The module is on the official openai-java SDK, and the Anthropic module is on anthropic-java. The exact Boot 4 names for the OTLP tracing properties and Hibernate 7's Jackson auto-detection were not confirmed from primary sources; both are marked verify above.