DEV Community

Cover image for Build Your First Spring AI Application with OpenAI Using Spring Boot
Ayush Shrivastava
Ayush Shrivastava

Posted on

Build Your First Spring AI Application with OpenAI Using Spring Boot

Build Your First Spring AI Application with OpenAI Using Spring Boot

If you have been following this Spring AI series, you already understand the two most important abstractions: ChatClient and ChatModel.

In the previous article, we learned that:

Your Java Code
      |
      v
 ChatClient
      |
      v
 ChatModel
      |
      v
 AI Provider
Enter fullscreen mode Exit fullscreen mode

ChatClient gives us a developer-friendly API, while ChatModel handles the underlying model integration.

Now it is time to build something real.

In this article, we will build our first Spring AI application using OpenAI.

We will start from project setup and configuration, connect Spring Boot to OpenAI, create a REST API, send prompts to an AI model, and understand what happens behind the scenes.

By the end, you will have a working AI-powered Spring Boot API.


What We Are Building

Our application will expose a simple REST endpoint:

GET /api/chat?message=Explain dependency injection
Enter fullscreen mode Exit fullscreen mode

The flow will look like this:

Client
   |
   v
Spring Boot REST API
   |
   v
ChatClient
   |
   v
ChatModel
   |
   v
OpenAI
   |
   v
AI Response
Enter fullscreen mode Exit fullscreen mode

The goal is intentionally simple.

We want to understand the complete flow before adding more advanced concepts such as RAG, tools, memory, structured output, and AI agents.


Prerequisites

Before starting, you should have:

  • Java installed
  • Maven installed
  • A Spring Boot project
  • An OpenAI API key
  • Basic knowledge of Spring Boot
  • Basic understanding of REST APIs

You should also be comfortable with dependency injection and creating REST controllers.

If you have followed the previous articles in this series, most of this should already be familiar.


Step 1: Create a Spring Boot Project

The easiest way to create the project is through Spring Initializr.

Choose:

Project: Maven

Language: Java

Spring Boot: Your compatible Spring Boot version

Packaging: Jar

Java: 17 or later
Enter fullscreen mode Exit fullscreen mode

For dependencies, we need Spring Web and the Spring AI OpenAI starter.

Your project will eventually look something like:

spring-ai-openai-demo
│
├── src
│   ├── main
│   │   ├── java
│   │   │   └── com.example.demo
│   │   │       └── DemoApplication.java
│   │   │
│   │   └── resources
│   │       └── application.properties
│
└── pom.xml
Enter fullscreen mode Exit fullscreen mode

Step 2: Add the Spring AI OpenAI Dependency

Add the Spring AI OpenAI model starter to your Maven configuration.

For example:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
Enter fullscreen mode Exit fullscreen mode

You will also need Spring Web:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
Enter fullscreen mode Exit fullscreen mode

Your project now has the components required to build a simple AI REST API.

Conceptually:

Spring Boot
     |
     +-- Spring Web
     |
     +-- Spring AI
             |
             +-- OpenAI integration
Enter fullscreen mode Exit fullscreen mode

Step 3: Configure Your OpenAI API Key

Spring AI needs credentials to communicate with OpenAI.

You can configure the API key using an environment variable.

For example:

export OPENAI_API_KEY=your-api-key
Enter fullscreen mode Exit fullscreen mode

On Windows PowerShell:

$env:OPENAI_API_KEY="your-api-key"
Enter fullscreen mode Exit fullscreen mode

Then reference it from your Spring configuration:

spring.ai.openai.api-key=${OPENAI_API_KEY}
Enter fullscreen mode Exit fullscreen mode

The important part is that your API key should not be hard-coded inside your Java source code.

Avoid doing this:

String apiKey = "sk-xxxxxxxx";
Enter fullscreen mode Exit fullscreen mode

Instead, keep secrets outside your source code.

A better approach is:

Environment Variable
        |
        v
Spring Configuration
        |
        v
Spring AI
        |
        v
OpenAI
Enter fullscreen mode Exit fullscreen mode

This becomes even more important when deploying your application to production.


Step 4: Configure the Chat Model

Spring AI needs to know which OpenAI chat model your application should use.

You can configure the model through your application properties.

For example:

spring.ai.openai.chat.options.model=your-model
Enter fullscreen mode Exit fullscreen mode

The exact model you choose depends on the models available to your OpenAI account and the requirements of your application.

The important concept is that your application does not need to manually construct OpenAI HTTP requests.

Spring AI handles that integration.


Step 5: Create the ChatController

Now let's create our first AI-powered controller.

@RestController
@RequestMapping("/api")
public class ChatController {

    private final ChatClient chatClient;

    public ChatController(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }

    @GetMapping("/chat")
    public String chat(@RequestParam("message") String message) {

        return chatClient
                .prompt(message)
                .call()
                .content();
    }
}
Enter fullscreen mode Exit fullscreen mode

Let's understand this carefully.


Understanding ChatClient Injection

The constructor receives:

ChatClient.Builder chatClientBuilder
Enter fullscreen mode Exit fullscreen mode

Spring AI provides this builder through Spring Boot auto-configuration when the appropriate model integration is configured.

We then create our ChatClient:

this.chatClient = chatClientBuilder.build();
Enter fullscreen mode Exit fullscreen mode

Now the controller has a ready-to-use AI client.

The architecture looks like:

Spring Boot
     |
     v
ChatClient.Builder
     |
     | build()
     v
ChatClient
Enter fullscreen mode Exit fullscreen mode

This is the same concept we explored in Part 2 of this series.


Step 6: Send Your First Prompt

Now let's call the API.

Start your Spring Boot application.

Then send:

GET /api/chat?message=What is dependency injection in Spring?
Enter fullscreen mode Exit fullscreen mode

The controller receives:

What is dependency injection in Spring?
Enter fullscreen mode Exit fullscreen mode

and passes it to:

chatClient
        .prompt(message)
        .call()
        .content();
Enter fullscreen mode Exit fullscreen mode

The flow is:

HTTP Request
     |
     v
ChatController
     |
     v
ChatClient
     |
     v
ChatModel
     |
     v
OpenAI
     |
     v
AI Response
Enter fullscreen mode Exit fullscreen mode

The generated response is returned to the client.

That's it.

You have now built a Spring Boot application that can communicate with an AI model.


Breaking Down the ChatClient API

Let's look at this code again:

chatClient
        .prompt(message)
        .call()
        .content();
Enter fullscreen mode Exit fullscreen mode

There are three important operations here.

prompt()

.prompt(message)
Enter fullscreen mode Exit fullscreen mode

This defines the prompt that you want to send to the model.

For example:

.prompt("Explain Java interfaces")
Enter fullscreen mode Exit fullscreen mode

or:

.prompt("Write a SQL query to find duplicate users")
Enter fullscreen mode Exit fullscreen mode

or:

.prompt(message)
Enter fullscreen mode Exit fullscreen mode

where message comes from an HTTP request.


call()

.call()
Enter fullscreen mode Exit fullscreen mode

This executes the model interaction.

You can think about it as:

Build Prompt
     |
     v
Call Model
Enter fullscreen mode Exit fullscreen mode

content()

.content()
Enter fullscreen mode Exit fullscreen mode

This extracts the generated text from the response.

So the entire chain:

chatClient
        .prompt(message)
        .call()
        .content();
Enter fullscreen mode Exit fullscreen mode

can be mentally understood as:

Create Prompt
     |
     v
Call AI Model
     |
     v
Extract Text
Enter fullscreen mode Exit fullscreen mode

This fluent style is one of the reasons ChatClient is convenient for application developers.


Step 7: Add a Service Layer

Although putting the AI call directly inside a controller works for a small demonstration, it is not how I would structure a production application.

Instead, let's introduce a service.

Our architecture becomes:

Client
  |
  v
Controller
  |
  v
Service
  |
  v
ChatClient
  |
  v
OpenAI
Enter fullscreen mode Exit fullscreen mode

Create:

@Service
public class ChatService {

    private final ChatClient chatClient;

    public ChatService(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    public String generateResponse(String message) {

        return chatClient
                .prompt(message)
                .call()
                .content();
    }
}
Enter fullscreen mode Exit fullscreen mode

Then our controller becomes:

@RestController
@RequestMapping("/api")
public class ChatController {

    private final ChatService chatService;

    public ChatController(ChatService chatService) {
        this.chatService = chatService;
    }

    @GetMapping("/chat")
    public String chat(@RequestParam String message) {

        return chatService.generateResponse(message);
    }
}
Enter fullscreen mode Exit fullscreen mode

This separation is much cleaner.

The controller handles HTTP.

The service handles AI interaction.


Why Separate the AI Logic?

Imagine that six months from now your application has:

ChatController
EmailController
SupportController
DocumentController
Enter fullscreen mode Exit fullscreen mode

If every controller directly interacts with the AI model, your code can quickly become difficult to maintain.

Instead:

Controllers
     |
     v
AI Services
     |
     v
ChatClient
     |
     v
ChatModel
Enter fullscreen mode Exit fullscreen mode

This keeps your application organized.

It also makes it easier to add features later.


Step 8: Add System Instructions

So far, we have only sent a user prompt.

But real AI applications usually need more control.

For example, imagine we are building a customer support assistant.

We don't want the model to behave like a generic chatbot.

We want to tell it:

You are a customer support assistant.
Answer clearly.
Keep responses concise.
Do not invent information.
Enter fullscreen mode Exit fullscreen mode

We can do that with a system message.

For example:

return chatClient
        .prompt()
        .system("""
                You are a helpful customer support assistant.
                Answer clearly and concisely.
                Do not invent information.
                """)
        .user(message)
        .call()
        .content();
Enter fullscreen mode Exit fullscreen mode

Now we have two different types of instructions:

System Message
      +
User Message
      |
      v
    Model
Enter fullscreen mode Exit fullscreen mode

This distinction will become extremely important later in the series.


System Message vs User Message

Think about the two messages like this.

System message

Defines the behavior of the assistant.

You are a Java programming assistant.
Always provide production-quality examples.
Enter fullscreen mode Exit fullscreen mode

User message

Contains the actual request.

Explain dependency injection.
Enter fullscreen mode Exit fullscreen mode

Together:

System
  |
  | "You are a Java assistant"
  |
  v
User
  |
  | "Explain dependency injection"
  |
  v
AI Model
Enter fullscreen mode Exit fullscreen mode

The PDF structure for this series introduces system and user messages as dedicated upcoming topics, so we will explore them in much more detail later.


Step 9: Build a Better AI Endpoint

Let's make our API slightly more realistic.

Instead of simply passing the user's message directly to the model, we can create a dedicated service method:

@Service
public class ChatService {

    private final ChatClient chatClient;

    public ChatService(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    public String ask(String question) {

        return chatClient
                .prompt()
                .system("""
                        You are a helpful Java and Spring Boot assistant.
                        Explain technical concepts clearly.
                        Use examples when appropriate.
                        """)
                .user(question)
                .call()
                .content();
    }
}
Enter fullscreen mode Exit fullscreen mode

Then:

@RestController
@RequestMapping("/api/chat")
public class ChatController {

    private final ChatService chatService;

    public ChatController(ChatService chatService) {
        this.chatService = chatService;
    }

    @GetMapping
    public String chat(@RequestParam String message) {

        return chatService.ask(message);
    }
}
Enter fullscreen mode Exit fullscreen mode

Now our API becomes:

GET /api/chat?message=What is Spring Boot?
Enter fullscreen mode Exit fullscreen mode

and the service controls how the AI behaves.


What Happens Behind the Scenes?

This simple line:

chatClient
        .prompt()
        .user(message)
        .call()
        .content();
Enter fullscreen mode Exit fullscreen mode

hides several operations.

Conceptually:

                    Spring Boot
                         |
                         v
                    ChatClient
                         |
                         v
                 Build Chat Request
                         |
                         v
                    ChatModel
                         |
                         v
                  OpenAI Integration
                         |
                         v
                    OpenAI API
                         |
                         v
                    AI Response
                         |
                         v
                    ChatClient
                         |
                         v
                       String
Enter fullscreen mode Exit fullscreen mode

This abstraction is one of the major benefits of Spring AI.

You focus on your application.

Spring AI handles the model integration.


What If OpenAI Changes Its API?

This is another reason abstractions are useful.

Without Spring AI, you might build:

Spring Boot
     |
     v
Custom HTTP Client
     |
     v
OpenAI API
Enter fullscreen mode Exit fullscreen mode

Your application would now contain provider-specific request and response handling.

With Spring AI:

Spring Boot
     |
     v
ChatClient
     |
     v
ChatModel
     |
     v
Provider
Enter fullscreen mode Exit fullscreen mode

Your application code stays focused on the AI interaction rather than provider-specific implementation details.

This is the abstraction we discussed in the previous article.


A Better Mental Model

If you're a Spring Boot developer, think about Spring AI in layers.

Application Layer
       |
       v
   ChatClient
       |
       v
    ChatModel
       |
       v
   AI Provider
       |
       v
    AI Model
Enter fullscreen mode Exit fullscreen mode

Each layer has a responsibility.

Your application

Business logic.

ChatClient

Developer-friendly AI interaction.

ChatModel

Model/provider abstraction.

AI Provider

OpenAI, Ollama, AWS Bedrock, and other supported providers.

AI Model

The actual language model generating the response.


Handling API Keys Correctly

One of the biggest mistakes beginners make is committing API keys to Git.

Never do this:

spring.ai.openai.api-key=sk-your-secret-key
Enter fullscreen mode Exit fullscreen mode

inside a repository that will be shared publicly.

Instead:

spring.ai.openai.api-key=${OPENAI_API_KEY}
Enter fullscreen mode Exit fullscreen mode

and configure the environment variable separately.

For local development:

OPENAI_API_KEY
Enter fullscreen mode Exit fullscreen mode

For production, use your deployment platform's secret management mechanism.

The principle is simple:

Source Code
     X
     |
     | No secrets
     |
Environment / Secret Store
     |
     v
Spring Boot
Enter fullscreen mode Exit fullscreen mode

Common Mistakes

Mistake 1: Putting the API Key in Git

Never commit secrets.

If your key is exposed, rotate it immediately.


Mistake 2: Calling the AI Model Directly From Every Controller

Avoid this structure:

Controller 1 -> AI
Controller 2 -> AI
Controller 3 -> AI
Controller 4 -> AI
Enter fullscreen mode Exit fullscreen mode

Prefer:

Controllers
     |
     v
Services
     |
     v
ChatClient
Enter fullscreen mode Exit fullscreen mode

Mistake 3: Treating ChatClient as the Model

Remember:

ChatClient != AI Model
Enter fullscreen mode Exit fullscreen mode

ChatClient is the application-facing abstraction.

The underlying model integration is handled through ChatModel.


Mistake 4: Sending Every Prompt Without Instructions

A production AI application usually needs some control over model behavior.

Instead of:

.prompt(message)
Enter fullscreen mode Exit fullscreen mode

you will often evolve toward:

.prompt()
.system("...")
.user(message)
Enter fullscreen mode Exit fullscreen mode

Later, we will see how prompt templates, advisors, memory, RAG, and tools make this even more powerful.


Mistake 5: Starting With Complex AI Architecture

You don't need this on day one:

RAG
 +
Vector Database
 +
Tools
 +
MCP
 +
Memory
 +
Agents
 +
Multiple Models
Enter fullscreen mode Exit fullscreen mode

Start with:

Spring Boot
     |
     v
ChatClient
     |
     v
OpenAI
Enter fullscreen mode Exit fullscreen mode

Then add complexity when your application actually needs it.


From Simple Chat to Real AI Backend

Our application currently looks simple:

Client
   |
   v
REST API
   |
   v
ChatClient
   |
   v
OpenAI
Enter fullscreen mode Exit fullscreen mode

But this architecture can grow.

For example:

                    Spring Boot
                         |
                         v
                    ChatClient
                         |
          +--------------+--------------+
          |              |              |
          v              v              v
        Memory          RAG           Tools
          |              |              |
          v              v              v
       History      Vector Store    Backend APIs
Enter fullscreen mode Exit fullscreen mode

This is where Spring AI becomes much more interesting.

A simple chatbot can eventually become a complete AI backend.


The Complete Example

Here is a simple production-style starting point.

ChatService

@Service
public class ChatService {

    private final ChatClient chatClient;

    public ChatService(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    public String ask(String question) {

        return chatClient
                .prompt()
                .system("""
                        You are a helpful Java and Spring Boot assistant.
                        Explain concepts clearly and provide examples when useful.
                        """)
                .user(question)
                .call()
                .content();
    }
}
Enter fullscreen mode Exit fullscreen mode

ChatController

@RestController
@RequestMapping("/api/chat")
public class ChatController {

    private final ChatService chatService;

    public ChatController(ChatService chatService) {
        this.chatService = chatService;
    }

    @GetMapping
    public String chat(@RequestParam String message) {

        return chatService.ask(message);
    }
}
Enter fullscreen mode Exit fullscreen mode

Configuration

spring.ai.openai.api-key=${OPENAI_API_KEY}

spring.ai.openai.chat.options.model=your-model
Enter fullscreen mode Exit fullscreen mode

The result is a clean architecture:

HTTP Client
    |
    v
ChatController
    |
    v
ChatService
    |
    v
ChatClient
    |
    v
ChatModel
    |
    v
OpenAI
Enter fullscreen mode Exit fullscreen mode

What You Learned

In this article, we built our first Spring AI application using OpenAI.

We learned how to:

  • Create a Spring Boot project
  • Add Spring AI's OpenAI integration
  • Configure an OpenAI API key
  • Configure a chat model
  • Inject ChatClient.Builder
  • Build a ChatClient
  • Send prompts to an AI model
  • Create an AI-powered REST endpoint
  • Separate controller and AI service logic
  • Use system and user messages
  • Keep API keys outside source code

Most importantly, you now understand the complete request flow:

Client
   |
   v
Spring Boot
   |
   v
Controller
   |
   v
Service
   |
   v
ChatClient
   |
   v
ChatModel
   |
   v
OpenAI
Enter fullscreen mode Exit fullscreen mode

This is the foundation for everything we will build later.


What's Next?

A hosted AI model is useful, but what if you want to run an LLM locally?

Maybe you don't want to send your data to an external provider.

Maybe you want to experiment without paying for API usage.

Maybe you're building an application where local inference is important.

That's where Ollama comes in.

In the next article, we will explore:

Part 4: Run Local LLMs with Ollama and Spring AI

We will install Ollama, run a local model, connect it to Spring AI, and see how the same ChatClient application can work with a local LLM.

That is one of the most interesting parts of Spring AI:

Same Application
       |
       +------> OpenAI
       |
       +------> Ollama
       |
       +------> Other Providers
Enter fullscreen mode Exit fullscreen mode

The application stays focused on the AI interaction while Spring AI handles the underlying model integration.


Frequently Asked Questions

What is Spring AI?

Spring AI is an abstraction layer that makes it easier for Spring applications to integrate with AI models and AI-related capabilities.


Can I use OpenAI with Spring Boot?

Yes. Spring AI provides an OpenAI integration that allows Spring Boot applications to communicate with OpenAI models.


Where should I store my OpenAI API key?

Do not hard-code it in your source code. Use environment variables or a proper secret-management solution.


Do I need ChatModel directly?

For most application-level use cases, you can work primarily with ChatClient. ChatModel remains important because it represents the underlying model integration.


Can I change the AI provider later?

One of the goals of Spring AI's abstraction model is to reduce application coupling to provider-specific implementation details.


What should I learn after this?

The next step in this series is running a local LLM with Ollama and Spring AI.


Spring AI Series

Part 1: Spring AI Tutorial: How Java Developers Can Build Generative AI Applications with Spring Boot

Part 2: ChatModel vs ChatClient in Spring AI

Part 3: Build Your First Spring AI Application with OpenAI

Part 4: Run Local LLMs with Ollama and Spring AI

Part 5: Run AI Models Locally with Docker Model Runner

Part 6: Using AWS Bedrock with Spring AI

Part 7: Working with Multiple Chat Models in Spring AI

Part 8: Understanding Message Roles in LLMs

Part 9: System Messages and User Messages in Spring AI

Part 10: Configuring Default Behavior in ChatClient

The series will continue into prompt templates, advisors, structured output, tokens, embeddings, chat memory, RAG, vector stores, tool calling, MCP, evaluation, observability, and AI agents.

Top comments (0)