Close

AI LangChain4j - Implementing a Custom ChatMemory

[Last Updated: Jan 19, 2026]

As we saw in the last tutorial how ChatMemoryStore is used.
ChatMemoryStore defines how chat messages are persisted and retrieved. LangChain4j separates memory management from storage, allowing custom persistence strategies.

Why a custom store?

Built-in stores cover common cases, but custom implementations are useful when integrating with existing storage systems or enforcing specific persistence rules.

Use cases

  • File-based chat history
  • Debugging and auditing conversations
  • Lightweight persistence without databases

Example

This custom implementation of ChatMemoryStore persist messages in JSON format to a file. We are using Jackson libarary to achieve that.

package com.logicbig.example;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.SystemMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.store.memory.chat.ChatMemoryStore;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;

public class FileChatMemoryStore implements ChatMemoryStore {
    private final Path file;
    private final ObjectMapper mapper = new ObjectMapper();

    FileChatMemoryStore(Path file) {
        this.file = file;
    }

    @Override
    public List<ChatMessage> getMessages(Object memoryId) {
        try {
            if (!Files.exists(file) || Files.size(file) == 0) {
                return new ArrayList<>();
            }

            List<JsonEntry> entries =
                    mapper.readValue(file.toFile(),
                                     new TypeReference<>() {});

            List<ChatMessage> messages = new ArrayList<>();

            for (JsonEntry entry : entries) {
                switch (entry.type) {
                    case "userMessage" ->
                            messages.add(UserMessage.from(entry.message));
                    case "systemMessage" ->
                            messages.add(SystemMessage.from(entry.message));
                    case "aiMessage" ->
                            messages.add(AiMessage.from(entry.message));
                }
            }

            return messages;

        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void updateMessages(Object memoryId, List<ChatMessage> newMessages) {
        try {
            List<JsonEntry> entries = new ArrayList<>();

            // append new entries
            for (ChatMessage message : newMessages) {
                entries.add(toEntry(message));
            }

            mapper.writerWithDefaultPrettyPrinter()
                  .writeValue(file.toFile(), entries);

        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void deleteMessages(Object memoryId) {
        try {
            Files.deleteIfExists(file);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private JsonEntry toEntry(ChatMessage message) {
        if (message instanceof UserMessage) {
            return new JsonEntry("userMessage", ((UserMessage) message).singleText());
        }
        if (message instanceof SystemMessage) {
            return new JsonEntry("systemMessage", ((SystemMessage) message).text());
        }
        if (message instanceof AiMessage) {
            return new JsonEntry("aiMessage", ((AiMessage) message).text());
        }
        throw new IllegalArgumentException("Unsupported message type");
    }

    static class JsonEntry {
        public String type;
        public String message;

        // required by Jackson
        public JsonEntry() {
        }

        JsonEntry(String type, String message) {
            this.type = type;
            this.message = message;
        }
    }
}

Interaction with LLM

In this example we are using Ollama with phi3:mini-128k model running locally.

package com.logicbig.example;

import dev.langchain4j.data.message.SystemMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.memory.ChatMemory;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.response.ChatResponse;
import dev.langchain4j.model.ollama.OllamaChatModel;
import dev.langchain4j.store.memory.chat.ChatMemoryStore;
import java.nio.file.Files;
import java.nio.file.Path;

public class FileChatMemoryStoreExample {

    public static void main(String[] args) throws Exception {

        ChatModel model = OllamaChatModel.builder()
                                         .baseUrl("http://localhost:11434")
                                         .modelName("phi3:mini-128k")
                                         .numCtx(4096)
                                         .temperature(0.7)
                                         .build();

        Path tempFile = Files.createTempFile("chat-memory-", ".txt");

        ChatMemoryStore store = new FileChatMemoryStore(tempFile);

        ChatMemory memory = MessageWindowChatMemory.builder()
                                                   .id("file-session")
                                                   .maxMessages(5)
                                                   .chatMemoryStore(store)
                                                   .build();

        memory.add(SystemMessage.from("You are a poet."));

        memory.add(UserMessage.from("Write a short poem on "
                                            + "Java programming language."));

        ChatResponse response = model.chat(memory.messages());
        memory.add(response.aiMessage());

        memory.add(UserMessage.from("Give a name to the poem."));

        ChatResponse response2 = model.chat(memory.messages());
        memory.add(response2.aiMessage());

        System.out.println("-- file json content --");
        Files.lines(tempFile).forEach(System.out::println);

    }

}

Output

-- file json content --
[ {
"type" : "systemMessage",
"message" : "You are a poet."
}, {
"type" : "userMessage",
"message" : "Write a short poem on Java programming language."
}, {
"type" : "aiMessage",
"message" : "In the realm of code and logic's flow, \nLies Java, steady as it goes. \nObject-oriented with grace, \nA mighty presence in cyberspace. \n\nBorn from Sun Microsystem's core, \nIts syntax feels like folklore. \nBytes compiled into a bytecode, \nIn the JVM where they bode. \n\nThreads run concurrent and free, \nJava handles with ease to see. \nA language diverse in scope, \nFrom mobile apps on every rope. \n\nWith interfaces as guiding light, \nPromising safety through night. \nException handling is key, \nIn the Java world we'll always be free."
}, {
"type" : "userMessage",
"message" : "Give a name to the poem."
}, {
"type" : "aiMessage",
"message" : "\"Java: A Digital Odyssey\""
} ]

Conclusion

The output confirms that messages are written to and restored from a file. This demonstrates how ChatMemoryStore cleanly decouples conversation state from persistence mechanics.

Example Project

Dependencies and Technologies Used:

  • langchain4j 1.10.0 (Build LLM-powered applications in Java: chatbots, agents, RAG, and much more)
  • langchain4j-ollama 1.10.0 (LangChain4j :: Integration :: Ollama)
  • jackson-databind 2.17.2 (General data-binding functionality for Jackson: works on core streaming API)
  • JDK 17
  • Maven 3.9.11

AI LangChain4j - Custom ChatMemoryStore (File) Select All Download
  • custom-chat-memory-store-file
    • src
      • main
        • java
          • com
            • logicbig
              • example
                • FileChatMemoryStoreExample.java

    See Also

    Join