skip to content

How does conversationId work in Spring AI ChatMemory, and how do you keep two users' conversations separate?

level: middleimportance: must knowfreq 65%

answer

  1. conversationId = partition key
  2. advisor param ChatMemory.CONVERSATION_ID
  3. .advisors(a -> a.param(...)) per request
  4. omit it -> shared 'default' bucket = bug
  5. clear(conversationId) to reset; you own lifecycle

basics

~20 s

conversationId 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 s

Every 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
java
@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

for a junior

Should know an id keys the conversation and different users need different ids.

for a middle

Should demonstrate passing ChatMemory.CONVERSATION_ID via .advisors(a -> a.param(...)) per request and know the default-bucket trap.

for a senior

Discusses per-session vs per-user ids, clear()/lifecycle, and the horizontal access-control risk of guessable ids.

for a principal

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

context