How does conversationId work in Spring AI ChatMemory, and how do you keep two users' conversations separate?
answer
- conversationId = partition key
- advisor param ChatMemory.CONVERSATION_ID
- .advisors(a -> a.param(...)) per request
- omit it -> shared 'default' bucket = bug
- clear(conversationId) to reset; you own lifecycle
basics
~20 sconversationId is the key that partitions stored messages. Each user/session gets its own id; you pass it per request via the advisor param ChatMemory.CONVERSATION_ID. If you omit it, everything shares the default id "default" and users see each other's history.
solid answer
~40 sEvery ChatMemory operation is scoped by a String conversationId — it's the partition key under which messages are stored and retrieved. The memory advisor reads a runtime advisor parameter named ChatMemory.CONVERSATION_ID to decide which conversation this request belongs to. You set it per call with the fluent API: `.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, userSessionId))`. If you never set it, all requests fall back to ChatMemory.DEFAULT_CONVERSATION_ID ("default"), which means every user shares one global history — a real correctness and privacy bug. So the pattern is: derive a stable id per user/session (e.g., authenticated user id or a chat session UUID), pass it on each request, and use ChatMemory.clear(conversationId) to reset or when the session ends. The id is opaque to Spring AI; you own its lifecycle.
code
java · 19 lines@RestController
class ChatController {
private final ChatClient chatClient;
ChatController(ChatClient.Builder builder, ChatMemory chatMemory) {
this.chatClient = builder
.defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
.build();
}
// sessionId identifies THIS user's conversation — never let it default
@PostMapping("/chat/{sessionId}")
String chat(@PathVariable String sessionId, @RequestBody String message) {
return chatClient.prompt()
.user(message)
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, sessionId))
.call()
.content();
}
}go deeper
Should know an id keys the conversation and different users need different ids.
Should demonstrate passing ChatMemory.CONVERSATION_ID via .advisors(a -> a.param(...)) per request and know the default-bucket trap.
Discusses per-session vs per-user ids, clear()/lifecycle, and the horizontal access-control risk of guessable ids.
Ties id strategy to isolation guarantees, cleanup/TTL, and multi-tenant privacy design.
## conversationId is the partition key Every method on `ChatMemory` takes a `String conversationId`: `add(conversationId, messages)`, `get(conversationId)`, `clear(conversationId)`. It is simply the key under which a conversation's messages are bucketed. Two different ids are two completely isolated histories; the same id is a continuation of the same dialogue. Spring AI treats the id as **opaque** — it does not generate, expire, or interpret it. You are responsible for choosing it and managing its lifecycle. ## How the advisor learns the id for a given request The memory advisors (`MessageChatMemoryAdvisor`, `PromptChatMemoryAdvisor`) look up the id from a **runtime advisor parameter**. The constant is `ChatMemory.CONVERSATION_ID` (its string value is `"chat_memory_conversation_id"`; in older milestone versions the constant lived on `AbstractChatMemoryAdvisor` as `CHAT_MEMORY_CONVERSATION_ID_KEY`). You supply it per request: ```java String answer = chatClient.prompt() .user(userText) .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, sessionId)) .call() .content(); ``` `a.param(...)` puts a value into the advisor context for *this* invocation only — perfect for a value that changes per user/request. You can also set a default id at builder time, but for a multi-user server it must be per-request. ## The default-id trap If you never pass a conversationId, the advisor falls back to `ChatMemory.DEFAULT_CONVERSATION_ID`, whose value is `"default"`. On a single-user demo that's fine. On a shared web server it is a serious bug: **every user's messages land in the same bucket**, so users see each other's history and answers, and the context balloons. This is one of the most common Spring AI mistakes. ## Choosing a good id - **Per authenticated user**: use the user id if a user has one long-lived conversation. - **Per session/thread**: use a generated UUID stored in the HTTP session or returned to the client, if a user can have multiple parallel chats. This is usually the safer default. - Make it **stable** across the turns you want linked and **unique** across the conversations you want isolated. ## Lifecycle - **Reset / "new chat"**: call `chatMemory.clear(conversationId)` to drop history. - **Cleanup**: with in-memory storage, abandoned conversations leak memory forever — you need eviction (or a persistent repository with TTL, e.g. Cassandra). With JDBC you'd prune rows. - **Security**: never derive the id from unauthenticated, user-supplied input in a way that lets one user guess/collide with another's id — that's a horizontal access-control hole exposing another user's conversation. ## Where it fits conversationId is the glue between a stateless HTTP endpoint and stateful conversation memory. Your controller extracts/creates the id (from auth or session), passes it into the advisor param, and Spring AI does the rest.
- What happens if you forget to set the conversationId on a multi-user server?Every request falls back to ChatMemory.DEFAULT_CONVERSATION_ID ("default"), so all users share one global history — they leak each other's messages and the shared context grows unbounded. A correctness and privacy bug.
- Where should the conversationId come from for a logged-in user who can run several chats at once?Not just the user id (that would merge all their chats). Generate a per-chat id (e.g. a UUID) tied to the authenticated user, so parallel threads stay isolated while remaining scoped to that user for access control.
saying these in an interview costs you the question
- Setting the conversationId once at builder time for all users on a shared server
- Not realizing omitting the id merges everyone into the 'default' conversation
- Deriving the id from guessable/unauthenticated input, enabling cross-user access
- Believing Spring AI generates or expires the id for you