DEV Community

HashCoder
HashCoder

Posted on

From Zero to AI Hero: Building a Gemini-Powered Chat App with Spring Boot (Part 1: The Foundation)

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:

  1. A user types a question into a simple web UI.
  2. Our ChatController receives the request.
  3. It passes the user's prompt to a dedicated MovieChatService.
  4. The MovieChatService constructs a carefully worded prompt (telling the AI its "persona") and sends it to the Gemini model via the Spring AI ChatClient.
  5. Simultaneously, we'll asynchronously save the conversation to a PostgreSQL database for logging and future analysis using a ChatLogService.
  6. 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
Enter fullscreen mode Exit fullscreen mode

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 AI openai-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 using gemini-2.0-flash, but you could easily switch to gemini-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;
    }
}
Enter fullscreen mode Exit fullscreen mode

Let's walk through this method:

  1. 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.
            """;
    
  2. User Prompt: We wrap the user's raw string in a PromptTemplate.

  3. Combine and Call: The modern Spring AI approach is to create a Prompt object containing a list of Message objects (in this case, our system and user messages). This structured format is what the ChatClient expects.

  4. 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 simple String. All that complexity, hidden behind a clean, fluent API.

  5. 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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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)