The world of Large Language Models (LLMs) is moving at a breakneck pace, and as Spring developers, we have a first-class ticket to the show: Spring AI. This incredible project from the Spring team demystifies the process of integrating powerful AI capabilities into our applications. It provides a unified, elegant abstraction over various AI providers, from OpenAI to Hugging Face, and, as we'll explore today, Google's powerful Gemini models.
In this series, we'll build a complete, production-minded AI application. For Part 1, we're laying the essential groundwork. We will create a simple yet robust "Movie Expert" chatbot. This bot will leverage the Google Gemini Pro model to answer movie-related questions, and we'll build it with all the Spring Boot best practices you know and love: dependency injection, service layers, and even asynchronous processing.
Ready to wire up your first AI? Let's get started.
The Big Picture: Our Application Architecture
Before we dive into the code, let's look at our application from 10,000 feet. The user flow is straightforward:
- A user types a question into a simple web UI.
- Our
ChatController
receives the request. - It passes the user's prompt to a dedicated
MovieChatService
. - The
MovieChatService
constructs a carefully worded prompt (telling the AI its "persona") and sends it to the Gemini model via the Spring AIChatClient
. - Simultaneously, we'll asynchronously save the conversation to a PostgreSQL database for logging and future analysis using a
ChatLogService
. - The AI's response is sent back to the user's screen.
Simple, clean, and scalable.
Setting the Stage: Configuration is Key
The magic of Spring AI begins in your application.properties
(or .yml
) file. This is where you tell Spring how to connect to the AI model of your choice. Here’s how we configure our application to use Google Gemini.
spring:
application:
name: Spring AI Chat
# --- Database Configuration ---
datasource:
url: jdbc:postgresql://localhost:5432/moviedb
username: admin
password: secret
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: create-drop # For demo purposes; use 'validate' or 'update' in prod
show-sql: true
properties:
hibernate:
format_sql: true
database: postgresql
database-platform: org.hibernate.dialect.PostgreSQLDialect
# --- Spring AI Configuration for Gemini ---
ai:
openai:
chat:
options:
model: gemini-2.0-flash # Or gemini-1.5-flash, etc.
base-url: https://generativelanguage.googleapis.com
# This path is crucial for the OpenAI-compatible endpoint
completions-path: /v1beta/models/{model}:generateContent
api-key: ${GEMINI_API_KEY} # Best practice: use environment variables
# --- Async Task Executor Configuration ---
task:
execution:
pool:
core-size: 5
max-size: 10
Let's break down the most important part—the spring.ai
block:
- Wait,
openai
? For Gemini? Yes, and this is a brilliant piece of engineering. Google provides an OpenAI-compatible endpoint for Gemini. This means we can use the battle-tested Spring AIopenai-spring-boot-starter
dependency and simply point it to Google's API. This gives us incredible flexibility without needing a separate, dedicated Gemini starter. -
model
: Here, we specify which Gemini model we want to use. I'm usinggemini-2.0-flash
, but you could easily switch togemini-1.5-flash
or another variant. -
base-url
: This is the root URL for Google's Generative Language API. -
completions-path
: This is the critical line. It tells Spring AI the exact path to use for generating content, which differs from the standard OpenAI path. Note: The path in the code provided in the prompt was slightly off; the official path for the REST API is/v1beta/models/{model}:generateContent
. -
api-key
: Your secret API key from the Google AI Studio. I strongly recommend loading this from an environment variable (${GEMINI_API_KEY}
) rather than hardcoding it.
We've also configured our database and a thread pool for asynchronous tasks, which we'll see in action shortly.
The Core Logic: Communicating with the AI
The MovieChatService
is the heart of our application. It's responsible for taking the user's raw input, giving it context, and interacting with the ChatClient
.
@Slf4j
@Service
public class MovieChatService {
private final ChatClient chatClient;
private final ChatLogService chatLogService;
// Constructor injection - a Spring best practice
public MovieChatService(ChatClient chatClient, ChatLogService chatLogService) {
this.chatClient = chatClient;
this.chatLogService = chatLogService;
}
public String getMovieChatResponse(String userPrompt) {
// 1. Define the AI's persona with a System Prompt
var systemPromptTemplate = new SystemPromptTemplate(Constants.MOVIE_PROMPT);
var systemMessage = systemPromptTemplate.createMessage();
// 2. Create a prompt from the user's direct input
var userPromptTemplate = new PromptTemplate(userPrompt);
var userMessage = userPromptTemplate.createMessage();
// 3. Combine prompts and create the final Prompt object
// NOTE: A more idiomatic Spring AI way is to pass a list of Messages
Prompt prompt = new Prompt(List.of(systemMessage, userMessage));
// 4. Call the AI model
String aiResponse = chatClient.prompt(prompt)
.call()
.content();
// 5. Asynchronously save the interaction
chatLogService.saveLog(userPrompt, aiResponse);
return aiResponse;
}
}
Let's walk through this method:
-
System Prompt: We use a
SystemPromptTemplate
to set the AI's persona. This is our "meta-instruction" that governs all its subsequent answers. We're telling it: "You are a helpful and knowledgeable movie expert." This is crucial for controlling the AI's tone and domain.
// Constants.java public static final String MOVIE_PROMPT = """ You are a helpful and knowledgeable movie expert. Your main goal is to answer questions about movies, actors, directors, and film history. Please be concise and friendly in your responses. If the user asks a question that is NOT related to movies, politely decline to answer and remind them that you are a movie expert. Give only 1 fact at a time. """;
User Prompt: We wrap the user's raw string in a
PromptTemplate
.Combine and Call: The modern Spring AI approach is to create a
Prompt
object containing a list ofMessage
objects (in this case, our system and user messages). This structured format is what theChatClient
expects.The Magic Call: The line
chatClient.prompt(prompt).call().content()
is the beautiful abstraction Spring AI provides. Under the hood, it's creating an HTTP request to the Gemini API, including your API key, formatting the JSON payload, sending the request, and parsing the response to give you back a simpleString
. All that complexity, hidden behind a clean, fluent API.Async Logging: We immediately fire off the
chatLogService.saveLog()
method. We don't wait for it to complete. This is a key performance optimization.
The Supporting Cast: Web Layer and Async Logging
To make our application whole, we need a few more pieces.
Asynchronous Logging: Why bother with @Async
? Imagine your database is slow or under heavy load. If we saved the log synchronously, the user would be stuck waiting for the database write to finish after the AI has already responded. By making it asynchronous (@EnableAsync
on the main class is required), we send the response back to the user instantly while the database operation proceeds in a separate thread from our configured pool. It’s a small change that drastically improves user experience.
@Service
public class ChatLogService {
// ... repository ...
@Async // Run this method in a background thread
public void saveLog(String userPrompt, String aiResponse) {
log.info("Saving chat log in a separate thread...");
ChatLog chatLog = new ChatLog();
// ... set properties and save ...
chatLogRepository.save(chatLog);
}
}
The Controller: Our ChatController
is a standard Spring MVC controller. It manages a simple in-memory list to display the conversation history on a Thymeleaf template. Its primary job is to orchestrate the flow between the user's browser and our backend services.
@Controller
public class ChatController {
// ... dependencies and chat history list ...
@PostMapping("/chat")
public String handleChat(@RequestParam String prompt, Model model) {
chatHistory.add(new ChatMessage("You", prompt));
String aiResponse = movieChatService.getMovieChatResponse(prompt);
chatHistory.add(new ChatMessage("MovieBot", aiResponse));
return "redirect:/chat"; // Redirect-after-post pattern
}
}
Conclusion and What's Next
And there you have it! In a surprisingly small amount of code, we've built the foundation of a robust, AI-powered chatbot. We've seen how to:
- Configure Spring AI to use Google's Gemini models via its OpenAI-compatible endpoint.
- Craft effective system and user prompts to guide the AI's behavior.
- Use the elegant
ChatClient
to handle all the complex API communication. - Implement a smart, non-blocking logging strategy using
@Async
.
This is just the beginning. Our bot is smart, but it has no knowledge of any specific data in our own database. In Part 2 of this series, we'll level up our application by introducing Retrieval-Augmented Generation (RAG). We'll teach our AI to query our movies
and actors
tables to answer questions with data it wasn't originally trained on. Stay tuned
Top comments (0)