Backend
Context aware Spring AI chat microservice
deepak marineni DEV Community
3 views
Keeping State in a Stateless World: Building a Context-Aware Gemini Microservice with Spring AI and H2
By default, Large Language Models (LLMs) are completely stateless. Every API call you make to Google Gemini is treated like a first-time introduction. If a user says "My name is Alex" in request one, and "What is my name?" in request two, Gemini will not know the answer.
To build a true chatbot experience, your microservice needs to remember the conversation history.
In this post, we will build a Spring Boot microservice that connects to the Gemini API, uses Spring AI's ChatMemory Advisor to maintain context, and backs up that history using a persistent H2 database mapped to unique user sessions.
The Architecture
Google Gemini API: Handles the natural language processing.
Spring AI ChatClient: Orchestrates the prompts and LLM interactions.
MessageChatMemoryAdvisor: Intercepts outgoing prompts to inject historical context.
H2 Database (H2ChatMemoryRepositoryDialect): Persists the chat history locally.
HTTP Session Header: Captures a unique session-id from the client to isolate individual user conversations.
1. Dependencies Setup
Add the required dependencies to your pom.xml. This includes the Spring AI starter for Gemini, the chat memory module, and the H2 database driver.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-h2console</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-chat-memory</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-google-genai</artifactId>
</dependency>
Configure Properties: Update the application.properties with chat model and get your API key from Google AI Studio
spring.ai.model.chat=google-genai
spring.ai.google.genai.api-key=API-KEY-GOES-HERE
spring.ai.google.genai.chat.model=gemini-3.5-flash
2. Configuring the Chat Memory
Instead of losing chat history on application restarts (which happens with default in-memory arrays), we configure a persistent ChatMemory bean backed by H2 using the JdbcChatMemoryRepository.
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.ai.chat.memory.repository.jdbc.H2ChatMemoryRepositoryDialect;
import org.springframework.ai.chat.memory.repository.jdbc.JdbcChatMemoryRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
@Configuration
public class ChatConfig {
@Bean
public JdbcChatMemoryRepository chatMemoryRepository(JdbcTemplate jdbcTemplate) {
// Enforces H2 syntax quirks for database execution
return JdbcChatMemoryRepository.builder().jdbcTemplate(jdbcTemplate)
.dialect(new H2ChatMemoryRepositoryDialect())
.build();
}
@Bean
public ChatMemory chatMemory(JdbcChatMemoryRepository repository) {
return MessageWindowChatMemory.builder()
.chatMemoryRepository(repository).build();
}
}
Configure properties: include the below in application.properties to maintain the session across application restarts and to enable h2-console
# H2 Database configuration (File-based storage to persist across restarts)
spring.datasource.url=jdbc:h2:file:~/data/demochat;DB_CLOSE_ON_EXIT=FALSE
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
# Enable H2 Console to view your tables manually at http://localhost:8080/h2-console
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
# Spring AI - Auto-initialize the schema for H2
spring.ai.chat.memory.repository.jdbc.initialize-schema=always
3. Service Layer
We build the ChatClient and attach the MessageChatMemoryAdvisor. This advisor acts as an interceptor: it fetches past messages from H2 before sending the prompt to Gemini, and saves Gemini's response right after.
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.stereotype.Service;
@Service
public class ChatService {
private final ChatClient chatClient;
public ChatService(ChatClient.Builder chatClientBuilder, ChatMemory chatMemory){
this.chatClient = chatClientBuilder.defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
.build();
}
public String getResponse(String sessionId, String queryString){
return this.chatClient.prompt(queryString)
.advisors(x -> x.param(ChatMemory.CONVERSATION_ID, sessionId))
.call().content();
}
}
Exposing the REST Endpoint
Finally, we expose a POST endpoint. We use @RequestHeader("x-session-id") to extract the unique tracking key provided by the client application.
import ai.chat_bot.gemini.demo.service.ChatService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
public class ChatController {
@Autowired
private ChatService chatService;
@PostMapping("/ai/prompt")
public String chat(@RequestHeader("x-session-id") String sessionId, @RequestBody String query){
return chatService.getResponse(sessionId, query);
}
}
Testing the setup
You can test the context awareness using curl or Postman.
Request establishing a context:
curl --location --request POST 'http://localhost:8080/api/ai/prompt' \
--header 'x-session-id: 8757788243' \
--header 'Content-Type: application/json' \
--data-raw '{
"query": "I love mangoes"
}'
Response: Mangoes are absolutely elite! There is a very good reason they are called the "King of Fruits." There is nothing quite like a perfectly ripe, juicy, sweet mango.
Request to verify history recall
curl --location --request POST 'http://localhost:8080/api/ai/prompt' \
--header 'x-session-id: 8757788243' \
--header 'Content-Type: application/json' \
--data-raw '{
"query": "what do you think is my favorite fruit"
}'
Response: I’m going to go out on a limb here and make a wild guess... mangoes? 🥭 😉
Call it a hunch, but you seemed pretty enthusiastic about them a moment ago!
If you send the second request with a different session-id value, Gemini will correctly respond that it doesn't know and can give a generic response proving that multi-user session isolation is fully working.
Conclusion
By combining Spring AI's Advisor API with an H2 JDBC store, you can turn a stateless LLM endpoint into a fully stateful, context-aware chatbot microservice with minimal boilerplate code.
You can view the source code directly in my GitHub Repository
Read original: https://dev.to/marinenimd/context-aware-spring-ai-chat-microservice-ake
← Previous
How I Built a Bilingual RAG-Powered AI Calling & Chat Agent (With a Full Admin Ops Center)
Next →
Built a self-hosted server control panel with Laravel + Livewire (broker/privilege-separation architecture)
Related
Built a self-hosted server control panel with Laravel + Livewire (broker/privilege-separation architecture)
Backend
0
Reddit r/php
AI Coding Assistants for Embedded Systems: 2026 Guide
Backend
5
Dev.to (EN Zone)
Tired of Rerunning Scripts? My Python REPL Secret for Instant Code Testing!
Backend
3
Dev.to (EN Zone)
Mainframe Migration Tools: What Works and What Fails
Backend
2
Dev.to (EN Zone)
Comments0
No comments yet — be the first