DEV Community

Cover image for Why Your AI Chatbot Forgets Everything — And How to Fix It
Sham Prakash K
Sham Prakash K

Posted on AI-assisted

Why Your AI Chatbot Forgets Everything — And How to Fix It

In the last article we built a working chat endpoint. Send a message, get a reply. It felt like magic.

Then I tried to have an actual conversation.

Me: "My name is Sham."
AI: "Hi Sham! How can I help you?"
Me: "What's my name?"
AI: "I don't have access to personal information about you."

The model had completely forgotten who I was. Not because it was broken — because of something fundamental about how LLMs work. Every API call is completely independent. The model has no memory between calls.

If you want it to remember anything, that's your problem to solve.

This article shows how — starting from the simplest possible solution, hitting its limits, then building the real one.

Why the model forgets

When you call the Gemini API, you send a list of messages. The model reads them, generates a reply, and the call ends. The next call starts completely fresh — the model has no idea the previous call ever happened.

So when the user sends message 5, the model only sees message 5. It has no knowledge of messages 1 through 4.

The fix is simple in concept: include all previous messages in every call. Send the full conversation history every time, so the model always has context.

Let's build that.


Solution 1 — A simple Map in memory

The simplest fix: a Map where the key is a session ID and the value is the list of messages for that session.

@RestController
@RequestMapping("/api/chat")
public class ChatController {

    private final ChatClient chatClient;

    // session ID → list of messages for that session
    private final Map<String, List<Message>> sessions = new ConcurrentHashMap<>();

    public ChatController(ChatClient.Builder builder) {
        this.chatClient = builder
            .defaultSystem("You are a helpful assistant.")
            .build();
    }

    @PostMapping("/session")
    public Map<String, String> startSession() {
        String sessionId = UUID.randomUUID().toString();
        sessions.put(sessionId, new ArrayList<>());
        return Map.of("sessionId", sessionId);
    }

    @PostMapping
    public String chat(@RequestBody ChatRequest request) {
        List<Message> history = sessions.getOrDefault(
            request.sessionId(), new ArrayList<>());

        // Add user message to history
        history.add(new UserMessage(request.message()));

        // Send full history to the model
        String reply = chatClient.prompt()
            .messages(history)
            .call()
            .content();

        // Add model reply to history
        history.add(new AssistantMessage(reply));
        sessions.put(request.sessionId(), history);

        return reply;
    }

    record ChatRequest(String sessionId, String message) {}
}
Enter fullscreen mode Exit fullscreen mode

Every user gets their own session ID. Their messages are stored in their own list. Every API call sends the full history for that session — so the model has context.

Now try the conversation:

Me: "My name is Sham."
AI: "Hi Sham! How can I help you?"
Me: "What's my name?"
AI: "Your name is Sham."

It works. Two different users with two different session IDs — completely separate conversations.

The code is simple. Every Java developer knows what a Map and a List are. No framework magic, just plain Java.


The problem with in-memory history

This works great — until you restart the server. All history is gone. Everyone's conversations, gone.

There's another problem: this is a single ArrayList shared across all users. User A and User B are in the same conversation. Not great.

And there's the token problem: a long conversation becomes thousands of tokens on every call, whether those old messages are relevant or not.

In-memory works for a quick demo. For anything real, you need persistent storage with session isolation.


Where to get a free PostgreSQL database

Before writing any code, you need a database. The easiest free option is Neon — serverless PostgreSQL, free tier, no credit card required.

  1. Go to neon.tech and sign up
  2. Create a new project — Neon gives you a PostgreSQL database instantly
  3. Copy the connection string from the dashboard — it looks like:
postgresql://username:password@ep-xxx.us-east-1.aws.neon.tech/dbname?sslmode=require
Enter fullscreen mode Exit fullscreen mode
  1. Set it as an environment variable:
DATABASE_URL=postgresql://username:password@...
Enter fullscreen mode Exit fullscreen mode

That's it. Free, no setup, no local PostgreSQL installation needed.


Solution 2 — PostgreSQL with Spring AI

Spring AI has a built-in JdbcChatMemoryRepository that stores conversation history in a database. Each conversation gets a unique ID — so different users are completely isolated from each other.

Step 1 — Add the dependency

<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>
Enter fullscreen mode Exit fullscreen mode

Step 2 — Create the table

Create src/main/resources/chat-memory-schema.sql:

CREATE TABLE IF NOT EXISTS chat_history (
    conversation_id VARCHAR(256) NOT NULL,
    content         TEXT         NOT NULL,
    type            VARCHAR(64)  NOT NULL,
    timestamp       TIMESTAMP    NOT NULL
);
Enter fullscreen mode Exit fullscreen mode

Step 3 — Tell Spring AI to use your table

By default Spring AI uses a table called SPRING_AI_CHAT_MEMORY. To use your own table name, implement JdbcChatMemoryRepositoryDialect:

public class ChatHistoryDialect implements JdbcChatMemoryRepositoryDialect {

    private static final String TABLE = "chat_history";

    @Override
    public String getSelectMessagesSql() {
        return "SELECT content, type FROM " + TABLE +
               " WHERE conversation_id = ? ORDER BY timestamp";
    }

    @Override
    public String getInsertMessageSql() {
        return "INSERT INTO " + TABLE +
               " (conversation_id, content, type, timestamp) VALUES (?, ?, ?, ?)";
    }

    @Override
    public String getSelectConversationIdsSql() {
        return "SELECT DISTINCT conversation_id FROM " + TABLE;
    }

    @Override
    public String getDeleteMessagesSql() {
        return "DELETE FROM " + TABLE + " WHERE conversation_id = ?";
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 4 — Wire it up in the controller

@RestController
@RequestMapping("/api/chat-ai")
public class SpringAiChatController {

    private final ChatClient chatClient;
    private final JdbcChatMemoryRepository memoryRepository;

    public SpringAiChatController(ChatClient.Builder builder, JdbcTemplate jdbcTemplate) {

        this.memoryRepository = JdbcChatMemoryRepository.builder()
            .jdbcTemplate(jdbcTemplate)
            .dialect(new ChatHistoryDialect())
            .build();

        // Keep last 20 messages — older ones are evicted automatically
        MessageWindowChatMemory memory = MessageWindowChatMemory.builder()
            .chatMemoryRepository(memoryRepository)
            .maxMessages(20)
            .build();

        this.chatClient = builder
            .defaultSystem("You are a helpful assistant.")
            .defaultAdvisors(MessageChatMemoryAdvisor.builder(memory).build())
            .build();
    }

    // Create a new session — returns a unique conversation ID
    @PostMapping("/session")
    public Map<String, String> startSession() {
        String conversationId = UUID.randomUUID().toString();
        return Map.of("conversationId", conversationId);
    }

    // Chat — pass the conversation ID with every message
    @PostMapping("/chat")
    public String chat(@RequestBody ChatRequest request) {
        return chatClient.prompt()
            .user(request.message())
            .advisors(a -> a.param("chat_memory_conversation_id", request.conversationId()))
            .call()
            .content();
    }

    // Delete a conversation
    @DeleteMapping("/session/{conversationId}")
    public Map<String, String> deleteSession(@PathVariable String conversationId) {
        memoryRepository.deleteByConversationId(conversationId);
        return Map.of("status", "deleted", "conversationId", conversationId);
    }

    record ChatRequest(String conversationId, String message) {}
}
Enter fullscreen mode Exit fullscreen mode

Step 5 — Configure application.properties

spring.datasource.url=${DATABASE_URL}
spring.datasource.username=${DATABASE_USERNAME}
spring.datasource.password=${DATABASE_PASSWORD}

spring.sql.init.schema-locations=classpath:chat-memory-schema.sql
spring.sql.init.mode=always
Enter fullscreen mode Exit fullscreen mode

mode=always forces the schema script to run on every startup. Without it, PostgreSQL (being a non-embedded database) won't run the script at all.


How it works now

Starting a conversation:

POST /api/chat-ai/session
→ { "conversationId": "a3f9b2c1-..." }
Enter fullscreen mode Exit fullscreen mode

Sending messages — pass the ID every time:

POST /api/chat-ai/chat
{ "conversationId": "a3f9b2c1-...", "message": "My name is Sham." }
→ "Hi Sham! How can I help you?"

POST /api/chat-ai/chat
{ "conversationId": "a3f9b2c1-...", "message": "What's my name?" }
→ "Your name is Sham."
Enter fullscreen mode Exit fullscreen mode

Spring AI fetches the last 20 messages for that conversation ID from PostgreSQL, includes them in the API call, saves the new exchange, and returns the response. You wrote none of that logic yourself.

Two users, two different conversation IDs — completely isolated. Server restarts — history survives. Long conversation — only the last 20 messages are sent, keeping tokens under control.


In-memory vs PostgreSQL — when to use which

Spring AI In-Memory Spring AI PostgreSQL
Setup Zero Database + dependency
Survives restart No Yes
Multi-user Yes — isolated by session ID Yes — isolated by session ID
Token control Automatic (maxMessages) Automatic (maxMessages)
Good for Local dev, quick demos Production, anything real

Start with in-memory locally. Switch to PostgreSQL before you deploy.


What's next

The chat app has memory now. Next up: deploying it to the cloud — Render, Docker, environment variables. Because a chat app that only runs on your laptop isn't a chat app, it's a script.


Have you hit the "it forgot everything" problem before understanding why? Drop it in the comments.

Sham Prakash K — Backend Engineer, 4+ years in Java, Spring Boot, and distributed systems. Building AI backend infrastructure. Writing about what I actually learned, mistakes included.

Top comments (0)