Amazon Bedrock lets a Spring Boot application use hosted models through AWS. With Spring AI, the application can call Bedrock through the same ChatClient API it uses for a local Ollama model.
This example adds both clients to OrderDesk, our demo store’s support assistant. We configure AWS access, explain the Bedrock Converse API, and let each request choose its backend. The recorded answers use Ollama; the Bedrock examples show the integration setup and an authentication failure without valid AWS credentials.
1. Understanding Amazon Bedrock and the Converse API
Amazon Bedrock provides access to models from several providers, including Anthropic, Amazon, Meta, and Mistral. It is a way to access those models through AWS, rather than a single model itself. Your application needs an AWS identity, appropriate permissions, and a supported model or inference profile.
Model families can expose different request formats and options. Without a shared interface, moving from one family to another could require changes to the code that builds each request.
The Converse API provides a shared message format for supported Bedrock chat models. An application sends messages, system instructions, and inference settings through the same API. Switching models can therefore be a configuration change, although features and accepted options still depend on the model. See the AWS Converse documentation.
A few AWS details are different from a local model in Ollama:
- AWS credentials. This example authenticates through the AWS SDK credentials chain rather than a provider API key.
- Model access. The account must be allowed to invoke the selected model. Access requirements differ between providers.
- Region. Choose a region and model or inference profile that support the operation you need.
- Model IDs and inference profiles. A value such as anthropic.claude-… identifies a model. A prefix such as us. can identify a cross-region inference profile. Check where that profile may route requests.
No successful Bedrock inference response is included. The recorded AWS error demonstrates an authentication failure; the complete answer shown later comes from Ollama. These results cannot compare Bedrock and Ollama answer quality or performance.
2. How Spring AI Connects to Both Backends
Spring AI represents a chat backend through ChatModel. Its Bedrock Converse implementation is BedrockProxyChatModel, and the Ollama implementation is OllamaChatModel. Each sits behind its own ChatClient, so the controller can use the same calling API for either backend:

There are two abstractions here. Spring AI gives the Java application a common chat API across backends. Bedrock Converse gives supported Bedrock model families a common request format. Neither guarantees identical model capabilities. The Spring AI Bedrock Converse reference explains the integration.
3. Configure the Spring Boot Application
The example is a separate Spring Boot project in the OrderDesk series called orderdesk-bedrock. You can find the project in the spring-ai-examples repository on GitHub. If you need help installing Ollama and downloading the qwen2.5:3b model, follow the Getting Started with Spring AI tutorial.
The project uses Spring Boot 4.1, Spring AI 2.0.1, and Java 21, with all classes in the com.howtodoinjava.orderdesk.bedrock package.
3.1. Dependencies
Here is the full pom.xml. It uses the Spring Boot 4.1.1 parent and imports the Spring AI BOM (bill of materials), which sets the version of every Spring AI module:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.1</version>
<relativePath/>
</parent>
<groupId>com.howtodoinjava</groupId>
<artifactId>orderdesk-bedrock</artifactId>
<version>1.0.0</version>
<properties>
<java.version>21</java.version>
</properties>
<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>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-ollama</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-bedrock-converse</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
The spring-ai-starter-model-bedrock-converse starter adds the BedrockProxyChatModel bean and the AWS SDK it uses. Both chat starters are enabled by default, so this app needs no extra property.
Don’t set spring.ai.model.chat in this app. That property selects one chat model for the whole app. With spring.ai.model.chat=bedrock-converse, the Ollama auto-configuration switches off, so there’s no OllamaChatModel bean and localClient has no model to use.
3.2. Configure AWS Credentials and Model Access
The local Ollama endpoint needs no account key. This Bedrock integration uses an AWS identity with permission to invoke the selected model.
The AWS SDK signs requests with credentials from a supported source. These can be a local development profile, temporary session credentials, or an IAM role attached to a deployed application. Creating permanent IAM-user keys is not a requirement of this integration.
The default credentials provider chain searches supported sources, including environment variables, local AWS profiles, and role credentials in AWS environments. Choose the source appropriate to where the application runs.
The example relies on that chain, so no access key or secret key is stored in application.properties. These environment variables illustrate one local setup. Temporary credentials also require a session token:
export AWS_ACCESS_KEY_ID=your-access-key-id
export AWS_SECRET_ACCESS_KEY=your-secret-access-key
An existing valid AWS profile may already supply credentials. Keep secrets outside source code and version control.
Next, confirm access to the selected model in the Bedrock console. Older instructions may refer to a separate Model Access menu:
Access requirements depend on the model and the permissions available to your AWS identity.
Follow the current AWS model access instructions. Many models are available with the required permissions, while some providers have additional onboarding steps. Confirm access for the model or inference profile used by the application.
Check the selected model’s pricing and quotas before making calls. Local inference avoids a hosted API charge but still uses your machine’s resources.
The existing example records this response when the application uses invalid AWS credentials:
software.amazon.awssdk.services.bedrockruntime.model.BedrockRuntimeException: The security token included in the request is invalid. (Service: BedrockRuntime, Status Code: 403, Request ID: 4f7a52a0-94f7-4ff6-bc30-c8f7dfad9887)
This is an AWS authentication error, rather than a model answer. Valid credentials are only one prerequisite: the request also needs the correct permissions, region, model access, and supported options. The example does not verify a successful call after those requirements are met.
3.3. Set the Region, Model, and Output Limit
Here is the full application.properties:
spring.application.name=orderdesk-bedrock
server.port=8086
# Two chat models in one app, so we build the ChatClients ourselves
spring.ai.chat.client.enabled=false
# The local model, as in the other OrderDesk articles
spring.ai.ollama.base-url=http://localhost:11434
spring.ai.ollama.chat.model=qwen2.5:3b
spring.ai.ollama.chat.temperature=0.2
# AWS credentials come from the default AWS chain: environment variables
# AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, or ~/.aws/credentials
spring.ai.bedrock.aws.region=us-east-1
spring.ai.bedrock.converse.chat.model=us.anthropic.claude-haiku-4-5-20251001-v1:0
spring.ai.bedrock.converse.chat.max-tokens=500
spring.ai.bedrock.converse.chat.temperature=0.2
The settings select where the request goes, which model handles it, and how much output the application allows.
The spring.ai.bedrock.aws.region property is the AWS region to call (us-east-1 is also the default).
The spring.ai.bedrock.converse.chat.model property selects the model ID or inference profile. The sample uses us.anthropic.claude-haiku-4-5-20251001-v1:0. Confirm that this value is available to your account and compatible with the configured region.
The output limit is 500 tokens. A low temperature reduces sampling variation where supported; it does not guarantee identical or correct answers.
4. Create Separate Bedrock and Ollama Clients
This application contains two ChatModel beans. It disables the auto-configured builder with spring.ai.chat.client.enabled=false and constructs two clients explicitly, binding each one to its intended model.
ChatConfig creates one client per model. Each client gets the same system prompt:
package com.howtodoinjava.orderdesk.bedrock;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.bedrock.converse.BedrockProxyChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ChatConfig {
static final String SUPPORT = "You are OrderDesk, the support assistant of an online store. "
+ "Answer in at most three sentences.";
// The hosted model: for questions that need a stronger model
@Bean
ChatClient hostedClient(BedrockProxyChatModel chatModel) {
return ChatClient.builder(chatModel).defaultSystem(SUPPORT).build();
}
// The local model in Ollama: free, for simple questions
@Bean
ChatClient localClient(OllamaChatModel chatModel) {
return ChatClient.builder(chatModel).defaultSystem(SUPPORT).build();
}
}
There are two ChatModel beans, so we ask for each one by its exact class: BedrockProxyChatModel and OllamaChatModel. ChatClient.builder(chatModel) then wraps each model in its own client.
The controller picks a client from the model request parameter. There’s no automatic routing: the caller decides, and the default is hosted. The controller returns the answer, the model name and the token counts. These values differ between models:
package com.howtodoinjava.orderdesk.bedrock;
// The answer plus the facts that differ between models
public record Reply(String model, String answer, Integer inputTokens, Integer outputTokens) {
}
package com.howtodoinjava.orderdesk.bedrock;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.metadata.ChatResponseMetadata;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SupportController {
private final ChatClient hostedClient;
private final ChatClient localClient;
SupportController(@Qualifier("hostedClient") ChatClient hostedClient,
@Qualifier("localClient") ChatClient localClient) {
this.hostedClient = hostedClient;
this.localClient = localClient;
}
// model=hosted (default) or model=local
@GetMapping("/ask")
Reply ask(@RequestParam String question,
@RequestParam(defaultValue = "hosted") String model) {
ChatClient client = "local".equals(model) ? localClient : hostedClient;
ChatResponse response = client.prompt().user(question).call().chatResponse();
ChatResponseMetadata metadata = response.getMetadata();
return new Reply(metadata.getModel(), response.getResult().getOutput().getText(),
metadata.getUsage().getPromptTokens(), metadata.getUsage().getCompletionTokens());
}
}
@Qualifier tells Spring which bean to inject when multiple beans match the required type. In this example, it identifies the bean by the name of the @Bean method. The chatResponse() method returns the whole response instead of only the text. This lets us read getMetadata(): the model that answered and the Usage with the token counts.
The main class is a plain Spring Boot application:
package com.howtodoinjava.orderdesk.bedrock;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class BedrockApplication {
public static void main(String[] args) {
SpringApplication.run(BedrockApplication.class, args);
}
}
5. Call the Endpoint and Read the Result
Start Ollama as shown in Getting Started. Then start the app with mvn spring-boot:run in the orderdesk-bedrock folder. It listens on port 8086.
Selecting model=local calls Ollama without invoking Bedrock. The recorded example starts the application with placeholder AWS credentials:
curl -G localhost:8086/ask --data-urlencode "question=Can I return a kettle after 5 days?" \
--data-urlencode model=local
{"model":"qwen2.5:3b","answer":"Yes, you can return a kettle within 5 days of receiving it, as many online stores offer a 7-day return policy, which typically includes the first 5 days. Please check the specific return policy of the retailer you purchased the kettle from for any additional details or requirements.","inputTokens":43,"outputTokens":58}
The recorded local response took about 10.6 seconds in the original setup. It is not a Bedrock timing.
The same question without model=local goes to Bedrock. With the fake keys, that’s the 403 error from Section 3.2. SupportController doesn’t catch the exception, so the caller gets Spring Boot’s default error response, an HTTP 500.
The answer invents a seven-day return window, although OrderDesk’s example policy allows ten days. This small application has not supplied that policy to the model. A hosted backend also needs store rules through instructions, retrieval, or tools; changing providers does not supply missing business data.
6. Switch to Another Supported Bedrock Model
To select another supported Bedrock model, change the configuration while retaining the Converse client. The following setting illustrates an Amazon Nova model:
spring.ai.bedrock.converse.chat.model=us.amazon.nova-lite-v1:0
The basic Java call stays the same because BedrockProxyChatModel continues to use Converse. Before switching, check model availability and the options your request sends.
Copy the exact model or inference profile ID from AWS documentation or the console, and confirm its availability and permissions. The sample setting has not been validated with a successful provider response.
The other thing is feature support. Not every model supports every Converse feature, such as a system prompt or tool calling. The Bedrock documentation lists which features each model supports, so check before you switch.
You can also set the model for one request instead of the whole app, with BedrockChatOptions. The Runtime Options section of the Spring AI Bedrock Converse page shows how.
7. Deployment Considerations
A few things change when real customers start using a model on Bedrock:
- When the app runs on AWS, give it an IAM role. Don’t put permanent keys in environment variables. The default credentials provider chain can also find role credentials, as the AWS SDK for Java docs describe.
- Give that role only the Bedrock permissions it needs, and only for the models you use.
- Monitor token usage, request volume, and model pricing. An output limit bounds one part of a request, rather than providing a complete spending limit.
- Handle authentication failures, missing permissions, throttling, timeouts, and service errors. The sample controller leaves exceptions unhandled. A local fallback requires an explicit application decision and is not implemented here.
- Keep the model id in application.properties. Model versions change over time, and with Converse a new version or even a new family is a configuration change.
The shared ChatClient API lets the application reuse its overall structure with Bedrock. Verify the selected model’s capabilities before relying on tools, particular response formats, or provider-specific options.
All articles in order: Spring AI Tutorial
Drop me your questions related to Spring AI with Amazon Bedrock in the comments.
Happy Learning !!