Course topics

By WebNest Studio

Spring Boot Tutorial

Chat Memory and Conversations

LLMs are stateless. Each request is independent; the model has no idea what the user said a moment ago unless you send that history again. A chatbot that forgets the user's name between two messages feels broken.

Spring AI solves this with the ChatMemory abstraction and memory advisors that automatically load previous messages before each call and save new ones afterwards. In this lesson you will add memory to a ChatClient, keep conversations separate per user, persist them in a database, and control how much history is sent so costs stay under control.

ChatMemory and ChatMemoryRepository

ChatMemory decides which messages to keep; a ChatMemoryRepository decides where to store them. Spring AI auto-configures a ChatMemory bean of type MessageWindowChatMemory — which keeps the most recent messages (20 by default) — backed by an in-memory repository. Swap the repository for JDBC, Cassandra, MongoDB or Neo4j to persist conversations.

Memory Advisors

Advisors wrap each ChatClient call. MessageChatMemoryAdvisor adds the stored history as proper user/assistant messages, which is the most accurate option. PromptChatMemoryAdvisor instead appends the history as text into the system prompt, useful for models with limited multi-message support. VectorStoreChatMemoryAdvisor stores messages in a vector store and retrieves only the most relevant past messages, which suits very long-running conversations.

Conversation IDs

Every memory advisor needs a conversation id, passed with .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, id)); omitting it throws an exception. The id decides which history is loaded, so derive it on the server from the authenticated user and a conversation or session id. Never accept a raw conversation id from the client without checking that it belongs to the current user — otherwise one user can read another user's chat.

Controlling History Size and Cost

Every remembered message is sent to the model on every call and billed as input tokens. A window of 10–20 messages is usually enough for chat. MessageWindowChatMemory evicts whole turns so a tool call is never separated from its result. For long conversations, consider summarising older messages, or use the vector-store memory advisor.

Examples

Without memory, the model forgets

Java
chatClient.prompt().user("Hi, my name is Asha and I'm learning Spring Security.").call().content();
chatClient.prompt().user("What's my name and what am I learning?").call().content();
Output
Hello Asha! Spring Security is a great topic...
I'm sorry, I don't know your name or what you are learning — could you tell me?

Adding MessageChatMemoryAdvisor with per-user conversations

Java
@Configuration
public class MemoryConfig {

    @Bean
    ChatClient tutorClient(ChatClient.Builder builder, ChatMemory chatMemory) {
        return builder
            .defaultSystem("You are a friendly Spring Boot tutor.")
            .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
            .build();
    }
}

@RestController
@RequestMapping("/api/tutor")
public class TutorController {

    private final ChatClient tutorClient;

    public TutorController(ChatClient tutorClient) {
        this.tutorClient = tutorClient;
    }

    @PostMapping("/{chatId}")
    public String chat(@PathVariable String chatId,
                       @RequestBody String message,
                       Principal principal) {
        // Scope the conversation to the logged-in user so ids cannot be guessed
        String conversationId = principal.getName() + ":" + chatId;
        return tutorClient.prompt()
            .user(message)
            .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId))
            .call()
            .content();
    }
}
Output
POST /api/tutor/1  "Hi, my name is Asha and I'm learning Spring Security."
-> Hi Asha! Spring Security is a great next step...
POST /api/tutor/1  "What's my name and what am I learning?"
-> Your name is Asha and you're learning Spring Security.
POST /api/tutor/2  "What's my name?"          (new conversation)
-> I don't know your name yet — what should I call you?

Persisting conversations with the JDBC repository

Java
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <scope>runtime</scope>
</dependency>

# application.yml
spring:
  ai:
    chat:
      memory:
        repository:
          jdbc:
            initialize-schema: always   # creates SPRING_AI_CHAT_MEMORY on startup

@Bean
ChatMemory chatMemory(JdbcChatMemoryRepository repository) {
    return MessageWindowChatMemory.builder()
        .chatMemoryRepository(repository)
        .maxMessages(12)
        .build();
}
Output
select conversation_id, type, left(content, 40) from spring_ai_chat_memory;
 asha:1 | USER      | Hi, my name is Asha and I'm learning Sp
 asha:1 | ASSISTANT | Hi Asha! Spring Security is a great nex
(Conversations now survive application restarts.)

Listing and clearing a user's conversation

Java
@GetMapping("/{chatId}/history")
public List<String> history(@PathVariable String chatId, Principal principal) {
    return chatMemory.get(principal.getName() + ":" + chatId).stream()
        .map(m -> m.getMessageType() + ": " + m.getText())
        .toList();
}

@DeleteMapping("/{chatId}")
public void clear(@PathVariable String chatId, Principal principal) {
    chatMemory.clear(principal.getName() + ":" + chatId);
}
Output
GET /api/tutor/1/history
["USER: Hi, my name is Asha and I'm learning Spring Security.","ASSISTANT: Hi Asha! ...","USER: What's my name...","ASSISTANT: Your name is Asha..."]

Common Mistakes

  • Using one fixed conversation id for every user, so all users share (and can see) the same history.
  • Trusting a conversation id sent by the browser without tying it to the authenticated user.
  • Keeping unlimited history, which grows token costs on every call and can exceed the model's context window.
  • Relying on the default in-memory repository in production and losing every conversation on restart or across instances.
  • Forgetting ChatMemory.CONVERSATION_ID in .advisors(...), which throws IllegalArgumentException at runtime.

Key Points to Remember

  • LLMs are stateless; memory advisors resend relevant history on each call.
  • ChatMemory (what to keep) is separate from ChatMemoryRepository (where to store it).
  • MessageChatMemoryAdvisor is the default choice; always pass a server-derived conversation id.
  • Use the JDBC (or another persistent) repository in production.
  • Limit history with MessageWindowChatMemory.maxMessages to control cost and context size.

Practice the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

Use your local JDK or project IDE for these examples. Codelab currently runs Python and HTML/CSS/JavaScript; framework examples may need project dependencies.