How to Implement AI Chat Memory in Spring Boot Using Spring AI AI for developers and Programmers Core Java Spring AI Spring Boot by devs5003 - July 16, 20260 How to Implement AI Chat Memory in Spring Boot Using Spring AI Imagine asking an AI assistant, “My name is John,” followed by “What’s my name?” If the assistant replies, “I don’t know,” your application feels broken. Modern AI applications must remember previous conversations to provide natural, context-aware interactions. This is exactly what Spring AI Chat Memory solves. Instead of manually storing and appending every previous message, Spring AI offers a clean abstraction that automatically manages conversation history. With just a few configuration changes, your chatbot can remember user preferences, previous questions, and ongoing discussions. In this tutorial, we’ll build a production-ready AI chatbot with memory using Spring Boot and Spring AI. By default, large language models are stateless: they forget everything the moment a response is sent. Spring AI solves this with a ChatMemory abstraction that stores and replays conversation history, so our chatbot remembers a user’s name, preferences, and earlier questions across multiple requests. This guide shows you exactly how to wire it up, from a quick in-memory setup to a production-ready JDBC-backed store. Table of Contents Toggle What You Will LearnPrerequisitesWhy This MattersWhat Is ChatMemory in Spring AI?How to Implement AI Chat Memory in Spring BootStep#1: Add the Spring AI DependenciesStep#2: Set Up In-Memory Chat MemoryStep#3: How Do You Persist Chat Memory to a Database?Step#4: Add your database connection to application.ymlStep#5: Managing Conversation IDs for Multiple UsersStep#6: Testing Chat Memory BehaviourStep#7: Handling Errors and Edge CasesCommon Mistakes to AvoidInterview Questions on This TopicFrequently Asked Questions (FAQs)ConclusionRelated What You Will Learn By the end of this tutorial, you will know how to: Add the correct Spring AI starters for chat memory Configure MessageWindowChatMemory with MessageChatMemoryAdvisor Persist conversation history to PostgreSQL using JdbcChatMemoryRepository Manage separate conversation threads per user with CONVERSATION_ID Write integration tests that verify memory actually works Avoid the eviction, tool-call, and concurrency pitfalls that trip up most beginners Prerequisites You need a working Spring Boot project with the Spring AI BOM configured in your pom.xml, and an active API key for your model provider (OpenAI, Anthropic, or a local Ollama setup). Basic familiarity with ChatClient, REST controllers, and Spring’s dependency injection is assumed. If you’re coming from plain Spring Boot without any AI integration yet, it helps to first build a simple /chat endpoint using ChatClient before layering memory on top. Why This Matters Every real chatbot, whether it’s a customer support bot, an interview-prep assistant, or an internal documentation helper, needs to recall what the user already said. In a production Spring Boot deployment, skipping chat memory means every follow-up question gets treated as a brand-new conversation, which frustrates users and breaks any multi-turn flow like ticket triage or step-by-step troubleshooting. There’s also a cost dimension that often gets overlooked. Because chat memory determines exactly which messages get sent back to the model on every call, a poorly tuned window size directly inflates your token bill on every single request. Interviewers frequently ask candidates to explain how they’d design stateful AI features on top of a stateless model, and ChatMemory is the textbook Spring AI answer, so understanding the trade-offs here pays off in both production systems and technical interviews. What Is ChatMemory in Spring AI? ChatMemory is the Spring AI interface responsible for storing and retrieving messages tied to a conversation ID, while a separate ChatMemoryRepository handles the actual storage mechanism. Think of ChatMemory as the decision-maker (which messages to keep, in what order, and when to trim) and ChatMemoryRepository as the filing cabinet (where those messages physically live, whether that’s RAM, PostgreSQL, Redis, or MongoDB). This separation matters in practice: it means you can swap PostgreSQL for Redis later without touching any of your business logic or controller code.docs. The most common implementation is MessageWindowChatMemory, which keeps a sliding window of the most recent messages (20 by default) and always preserves system messages when older ones are evicted. Unlike a naive fixed-size buffer that could cut a conversation mid-exchange, MessageWindowChatMemory evicts whole conversational turns together, so a tool call never gets separated from its response, and a user question never gets orphaned from the assistant’s reply. There’s a second advisor family worth knowing about here too. MessageChatMemoryAdvisor injects prior messages directly into the model’s context as structured chat messages, while PromptChatMemoryAdvisor instead flattens the history into plain text inside the system prompt. Most production setups use MessageChatMemoryAdvisor because it preserves message roles (user, assistant, system) correctly, which matters for models that reason differently based on who said what. How to Implement AI Chat Memory in Spring Boot Step#1: Add the Spring AI Dependencies Start with the core chat starter for your model provider. For persistent memory, you also need a repository starter. org.springframework.ai spring-ai-starter-model-openai org.springframework.ai spring-ai-starter-model-chat-memory-repository-jdbc org.postgresql postgresql runtime If you skip the JDBC starter, Spring AI auto-configures InMemoryChatMemoryRepository for you, which is fine for local testing but loses everything on restart. Make sure your Spring AI BOM version matches your Spring Boot version; mismatched versions are one of the most common causes of NoSuchBeanDefinitionException errors when wiring up ChatMemory beans. Step#2: Set Up In-Memory Chat Memory For prototyping, wire MessageWindowChatMemory directly into your ChatClient using MessageChatMemoryAdvisor. @RestController public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient.Builder builder) { ChatMemory chatMemory = MessageWindowChatMemory.builder() .maxMessages(10) .build(); this.chatClient = builder .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build()) .build(); } @GetMapping("/chat") public String chat(@RequestParam String message, @RequestParam String conversationId) { return chatClient.prompt() .user(message) .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId)) .call() .content(); } } Notice the CONVERSATION_ID parameter. Spring AI made this required in recent releases; any call that omits it throws an IllegalArgumentException at runtime, since there is no default conversation ID anymore. This is a change many older tutorials online still miss because they were written against earlier Spring AI milestones where a default ID existed. Run this locally and send two requests with the same conversationId, first “My name is Rahul”, then “What’s my name?”. The second response should correctly say Rahul, confirming the memory advisor is retrieving and replaying the earlier message before the new prompt goes to the model. Step#3: How Do You Persist Chat Memory to a Database? Persisting chat memory means backing ChatMemory with JdbcChatMemoryRepository so history survives application restarts and works correctly across multiple instances behind a load balancer. Configure it once as a Spring bean, then pass it into MessageWindowChatMemory, and every conversation is automatically saved to a relational database table. @Bean public ChatMemory chatMemory(JdbcChatMemoryRepository jdbcChatMemoryRepository) { return MessageWindowChatMemory.builder() .chatMemoryRepository(jdbcChatMemoryRepository) .maxMessages(20) .build(); } @Bean public ChatClient chatClient(ChatClient.Builder builder, ChatMemory chatMemory) { return builder .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build()) .build(); } Step#4: Add your database connection to application.yml spring: datasource: url: jdbc:postgresql://localhost:5432/chatmemory_db username: postgres password: your_password ai: chat: memory: repository: jdbc: initialize-schema: never Spring AI auto-creates the SPRING_AI_CHAT_MEMORY table on startup for embedded databases like H2, but for PostgreSQL or MySQL in production you should manage the schema through Flyway or Liquibase instead, which is exactly why initialize-schema is set to never above. One gotcha worth flagging clearly: JdbcChatMemoryRepository silently drops AssistantMessage instances that contain tool calls, along with their corresponding ToolResponseMessage replies, on save. So if your chatbot relies heavily on tool calling, for example calling a weather API or a database lookup mid-conversation, you will lose that part of the exchange unless you switch to a custom repository implementation. For applications where memory needs to scale beyond exact recent messages, such as retrieving semantically relevant older context from a long conversation history, VectorStoreChatMemoryAdvisor retrieves conversation history from a vector store and injects it into the system message as plain text instead of a structured message list. This is useful for long-running support conversations where the relevant context might be from ten exchanges ago, not just the last two. Step#5: Managing Conversation IDs for Multiple Users Every distinct user or session needs its own conversationId string, typically the user’s session token, account ID, or a generated UUID per chat thread. Pass this value on every request through the advisor parameter, since mixing conversation IDs across users will leak one person’s chat history into another’s response, which is both a bug and a serious privacy problem. @GetMapping("/chat") public String chat(@RequestParam String message, Principal principal) { String conversationId = "user-" + principal.getName(); return chatClient.prompt() .user(message) .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId)) .call() .content(); } In a multi-tenant SaaS product, storing conversationId alongside the authenticated user ID in your own application database (not just Spring AI’s internal table) makes it much easier to audit conversations, build a “chat history” UI feature for users, and support GDPR-style data deletion requests later. Derive the ID from something stable like the authenticated Principal, never from client-supplied input alone, since a client could otherwise spoof another user’s conversation ID. Step#6: Testing Chat Memory Behaviour Chat memory is easy to get subtly wrong, so writing a focused integration test pays off. Use Spring’s test slice support along with a mocked or fake ChatModel to verify memory persistence without spending on real API calls. @SpringBootTest class ChatMemoryIntegrationTest { @Autowired private ChatMemory chatMemory; @Test void shouldRetainMessagesAcrossCalls() { String conversationId = "test-conversation-1"; chatMemory.add(conversationId, new UserMessage("My name is Priya")); chatMemory.add(conversationId, new AssistantMessage("Nice to meet you, Priya!")); List history = chatMemory.get(conversationId); assertThat(history).hasSize(2); assertThat(history.get(0).getContent()).contains("Priya"); } } This test confirms two things at once: that messages are actually stored under the right conversation ID, and that retrieval returns them in the correct order. In a production Spring Boot deployment, you will typically want a second test that pushes more messages than maxMessages and asserts that eviction correctly drops the oldest turn while keeping the system message intact. Step#7: Handling Errors and Edge Cases A few edge cases show up consistently once chat memory moves from a demo into real traffic. First, handle the missing conversationId case explicitly at the controller layer instead of letting the raw IllegalArgumentException bubble up as an unhandled 500 error: @ExceptionHandler(IllegalArgumentException.class) public ResponseEntity handleMissingConversationId(IllegalArgumentException ex) { return ResponseEntity.badRequest() .body("A valid conversationId is required for this request."); } Second, watch for concurrent writes to the same conversationId from rapid duplicate requests, such as a user double-clicking send. MessageWindowChatMemory is not inherently transactional against the underlying JDBC table under high concurrency, so for high-traffic endpoints, consider adding idempotency keys or rate limiting per conversation. Third, if you switch model providers, remember that message role handling can differ slightly, so test memory retrieval against your specific provider rather than assuming behaviour is identical across OpenAI, Anthropic, and Ollama. Common Mistakes to Avoid Forgetting to pass CONVERSATION_ID, which throws IllegalArgumentException since Spring AI removed the default conversation ID. Relying on InMemoryChatMemoryRepository in production, where restarts or multiple instances silently wipe or fragment history. Assuming JdbcChatMemoryRepository stores tool-calling exchanges; it filters them out on save. Setting maxMessages too small for multi-step tool-calling flows, which can evict an entire turn before a new user message arrives. Confusing chat memory (contextual recall for the model) with chat history (the full audit trail for your application), which need different storage strategies. Trusting a client-supplied conversationId without tying it to an authenticated user, which risks leaking one user’s history into another’s session. Skipping integration tests for eviction behaviour, only to discover in production that long conversations silently lose earlier context. Interview Questions on This Topic Q#1. What is the difference between ChatMemory and ChatMemoryRepository in Spring AI? Ans: ChatMemory is the higher-level contract that decides what messages to keep in a conversation, applying logic like windowing, eviction, and system-message handling before messages ever reach the model. ChatMemoryRepository is the lower-level storage contract that only knows how to save, retrieve, and delete raw messages for a given conversation ID, with no opinion on eviction policy. ChatMemory: decision layer (e.g., MessageWindowChatMemory decides which 20 messages survive) ChatMemoryRepository: storage layer (e.g., InMemoryChatMemoryRepository, JdbcChatMemoryRepository) that physically persists whatever ChatMemory tells it todocs. A good interview framing: ChatMemory is the “brain” that curates context; ChatMemoryRepository is the “filing cabinet” it delegates storage to, which is why you can swap Postgres for Redis without touching your business logic. Q#2. How does MessageWindowChatMemory decide which messages to evict? Ans: MessageWindowChatMemory enforces a fixed maximum message count (default 20) and evicts the oldest messages once that limit is exceeded. It applies special handling for SystemMessage: once added, a system message is always preserved during eviction, and if a new system message with different content arrives, it replaces the previous one rather than stacking up. The Spring AI implementation is directly aligned with the underlying pattern used across memory implementations (including LangChain4j’s identical-named class): if an AssistantMessage containing a tool call is evicted, its orphaned tool-response message is also automatically evicted, since several providers like OpenAI reject requests containing an orphaned tool response with no matching request. This ensures the window never leaves a “dangling” tool exchange that would break the next API call. Q#3. Why is CONVERSATION_ID mandatory in recent Spring AI versions? Ans: Spring AI removed the implicit default conversation ID in recent versions, requiring every memory-backed request to explicitly supply ChatMemory.CONVERSATION_ID as an advisor parameter. This design choice exists because a silent default ID would cause every unauthenticated or misconfigured request to share the same conversation bucket, mixing unrelated users’ histories together without any error signal. Making the ID mandatory forces developers to explicitly design conversation scoping (per user, per session, or per chat thread) rather than accidentally relying on a global fallback, and it fails fast with an IllegalArgumentException at development time instead of causing a subtle data-leak bug in production. Q#4. When would you choose VectorStoreChatMemoryAdvisor over MessageChatMemoryAdvisor? Ans: Choose based on how the conversation history needs to reach the model: Aspect MessageChatMemoryAdvisor VectorStoreChatMemoryAdvisor How history is injected As structured chat messages (user/assistant roles preserved) As plain text merged into the system message Best for Short-to-medium recent context, exact recall of last N turns Long conversations where only semantically relevant older turns matter Underlying store Any ChatMemoryRepository (JDBC, in-memory, Redis) A vector store (embeddings-based similarity search) Token efficiency Can waste tokens on irrelevant recent messages if window is large Retrieves only the most relevant snippets, saving tokens Use MessageChatMemoryAdvisor for typical multi-turn chatbots where the last 10-20 messages are usually all that matters. Switch to VectorStoreChatMemoryAdvisor when conversations run very long and the useful context might be buried far back, such as a support thread where a detail mentioned 50 messages ago is still relevant. Q#5. How would you design conversation ID generation for a multi-tenant application? Ans: For a multi-tenant application, the conversation ID should be derived server-side from trusted identity data, never accepted directly from client input, to prevent one tenant’s conversation from leaking into another’s. A robust pattern combines multiple identity dimensions into a single deterministic or generated key.docs.spring+1 Structure the ID as a composite: tenantId:userId:sessionId or tenantId-userId-threadUUID, so isolation is enforced by construction, not convention Derive userId from the authenticated Principal on the request, never from a request parameter a client could spoof Generate a new UUID per distinct chat thread if a user can have multiple concurrent conversations (e.g., separate support tickets) Store the mapping of conversationId to tenant and user in your own application table, not just Spring AI’s internal memory table, so you can audit, filter, and delete conversations per GDPR-style requests Enforce authorization checks at the controller layer so a user can only pass a conversationId prefix matching their own tenant and user ID This approach keeps tenant isolation as a structural guarantee rather than something that depends on every developer remembering to filter correctly at query time. Frequently Asked Questions (FAQs) What is chat memory in Spring AI? Chat memory in Spring AI is the mechanism that stores and retrieves relevant messages for a conversation so a large language model can maintain context across multiple user turns. How do I add chat memory to a Spring Boot ChatClient? Build a ChatMemory instance like MessageWindowChatMemory, wrap it in MessageChatMemoryAdvisor, and register it as a default advisor on your ChatClient.Builder. Does Spring AI chat memory persist after a restart? Only if you back it with a persistent repository such as JdbcChatMemoryRepository, MongoChatMemoryRepository, or RedisChatMemoryRepository. The default InMemoryChatMemoryRepository loses everything on restart. What is the default message window size in Spring AI? MessageWindowChatMemory defaults to a window of 20 messages, evicting the oldest complete turns first while always keeping system messages Can Spring AI chat memory handle tool calls? Not fully in JDBC, Cassandra, or MongoDB repositories today; these silently filter out AssistantMessage tool calls and their responses on save. What database does Spring AI support for chat memory? JdbcChatMemoryRepository supports PostgreSQL, MySQL/MariaDB, SQL Server, HSQLDB, and Oracle Database out of the box via a dialect abstraction. Conclusion Chat memory turns a stateless LLM call into a genuinely conversational Spring Boot application, and MessageWindowChatMemory combined with a JDBC or Redis-backed repository is the practical starting point for most production use cases. Next, explore how to combine chat memory with Retrieval-Augmented Generation (RAG) so your assistant can recall both conversation context and external knowledge sources at the same time. References “Spring AI Chat Memory reference” “ChatMemory interface Javadoc” You may also like: AI Agents in Spring Boot: Building Autonomous Workflows with Spring AI Spring AI RAG with pgvector tutorial 12 Essential AI Terms Java Developer Must Know in 2026 Top 10 AI Tools for Java Developers and Programmers in 2026 Best AI tools for Java Developers by Development Phase Free AI Framework for Java Developers in 2026: Think Like a Technology PRO (Try It Now) Related