CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-dev-langchain4j--langchain4j-core

Core classes and interfaces of LangChain4j providing foundational abstractions for LLM interaction, RAG, embeddings, agents, and observability

Overview
Eval results
Files

memory.mddocs/

Chat Memory

Package: dev.langchain4j.memory.chat Thread-Safety: Most implementations are NOT thread-safe - synchronize externally Use Case: Persistent conversation history, context management, multi-turn conversations

Chat memory provides storage and retrieval of conversation history, enabling multi-turn conversations with context awareness.

Core Interfaces

ChatMemory

package dev.langchain4j.memory.chat;

import dev.langchain4j.data.message.ChatMessage;

/**
 * Storage for conversation history
 * Thread-Safety: NOT thread-safe - synchronize externally
 */
public interface ChatMemory {
    /**
     * Get unique identifier for this memory
     * @return Memory ID
     */
    Object id();

    /**
     * Add message to memory
     * @param message Message to add
     */
    void add(ChatMessage message);

    /**
     * Get all messages from memory
     * @return List of all messages
     */
    List<ChatMessage> messages();

    /**
     * Get messages with limit
     * @param maxMessages Maximum number of recent messages
     * @return List of recent messages
     */
    List<ChatMessage> messages(int maxMessages);

    /**
     * Clear all messages
     */
    void clear();
}

Memory Implementations

MessageWindowChatMemory

package dev.langchain4j.memory.chat;

/**
 * In-memory chat memory with sliding window
 * Keeps only the N most recent messages
 * Thread-Safety: NOT thread-safe
 */
public class MessageWindowChatMemory implements ChatMemory {
    private final Object id;
    private final int maxMessages;
    private final List<ChatMessage> messages;

    public static MessageWindowChatMemory withMaxMessages(int maxMessages) { /* ... */ }

    public static Builder builder() { /* ... */ }

    public static class Builder {
        public Builder id(Object id) { /* ... */ }
        public Builder maxMessages(int maxMessages) { /* ... */ }
        public MessageWindowChatMemory build() { /* ... */ }
    }

    @Override
    public void add(ChatMessage message) {
        messages.add(message);
        // Evict oldest if exceeds max
        while (messages.size() > maxMessages) {
            messages.remove(0);
        }
    }
}

Usage:

// Create memory for single conversation
ChatMemory memory = MessageWindowChatMemory.withMaxMessages(20);

// Add messages
memory.add(SystemMessage.from("You are a helpful assistant."));
memory.add(UserMessage.from("Hello!"));
memory.add(AiMessage.from("Hi! How can I help you today?"));

// Get conversation history
List<ChatMessage> history = memory.messages();
ChatResponse response = chatModel.chat(history);

ChatMemoryStore

package dev.langchain4j.memory.chat;

/**
 * Persistent storage for multiple chat memories
 * Thread-Safety: Implementation-dependent
 */
public interface ChatMemoryStore {
    /**
     * Get messages for a memory ID
     * @param memoryId Unique memory identifier
     * @return List of messages (empty if not found)
     */
    List<ChatMessage> getMessages(Object memoryId);

    /**
     * Update messages for a memory ID
     * @param memoryId Unique memory identifier
     * @param messages New list of messages
     */
    void updateMessages(Object memoryId, List<ChatMessage> messages);

    /**
     * Delete messages for a memory ID
     * @param memoryId Unique memory identifier
     */
    void deleteMessages(Object memoryId);
}

Store Implementations

InMemoryChatMemoryStore

package dev.langchain4j.memory.chat;

import java.util.concurrent.ConcurrentHashMap;

/**
 * In-memory chat memory store
 * Data lost on restart
 * Thread-Safety: Thread-safe (uses ConcurrentHashMap)
 */
public class InMemoryChatMemoryStore implements ChatMemoryStore {
    private final Map<Object, List<ChatMessage>> store = new ConcurrentHashMap<>();

    @Override
    public List<ChatMessage> getMessages(Object memoryId) {
        return store.getOrDefault(memoryId, new ArrayList<>());
    }

    @Override
    public void updateMessages(Object memoryId, List<ChatMessage> messages) {
        store.put(memoryId, new ArrayList<>(messages));
    }

    @Override
    public void deleteMessages(Object memoryId) {
        store.remove(memoryId);
    }
}

Multi-User Memory

ChatMemoryProvider

package dev.langchain4j.memory.chat;

/**
 * Provides chat memory instances per user/session
 */
public interface ChatMemoryProvider {
    /**
     * Get or create memory for a memory ID
     * @param memoryId Unique identifier (user ID, session ID, etc.)
     * @return Chat memory instance
     */
    ChatMemory get(Object memoryId);
}

Default Implementation

/**
 * Multi-user memory management
 */
public class MultiUserChatMemory implements ChatMemoryProvider {
    private final ChatMemoryStore store;
    private final int maxMessages;

    public MultiUserChatMemory(ChatMemoryStore store, int maxMessages) {
        this.store = store;
        this.maxMessages = maxMessages;
    }

    @Override
    public ChatMemory get(Object memoryId) {
        return MessageWindowChatMemory.builder()
            .id(memoryId)
            .maxMessages(maxMessages)
            .chatMemoryStore(store)
            .build();
    }
}

Usage Examples

Single User Conversation

public class SimpleChatbot {
    private final ChatModel chatModel;
    private final ChatMemory memory;

    public SimpleChatbot(ChatModel chatModel) {
        this.chatModel = chatModel;
        this.memory = MessageWindowChatMemory.withMaxMessages(20);

        // Add system message
        memory.add(SystemMessage.from("You are a helpful coding assistant."));
    }

    public String chat(String userMessage) {
        // Add user message
        memory.add(UserMessage.from(userMessage));

        // Get response with full history
        ChatResponse response = chatModel.chat(memory.messages());

        // Add AI response to memory
        memory.add(response.aiMessage());

        return response.aiMessage().text();
    }
}

Multi-User Chatbot

public class MultiUserChatbot {
    private final ChatModel chatModel;
    private final ChatMemoryProvider memoryProvider;

    public MultiUserChatbot(ChatModel chatModel) {
        this.chatModel = chatModel;

        // Persistent store (could be Redis, PostgreSQL, etc.)
        ChatMemoryStore store = new InMemoryChatMemoryStore();

        this.memoryProvider = new MultiUserChatMemory(store, 20);
    }

    public String chat(String userId, String userMessage) {
        // Get memory for this user
        ChatMemory memory = memoryProvider.get(userId);

        // Ensure system message exists
        if (memory.messages().isEmpty()) {
            memory.add(SystemMessage.from("You are a helpful assistant."));
        }

        // Add user message
        memory.add(UserMessage.from(userMessage));

        // Generate response
        ChatResponse response = chatModel.chat(memory.messages());

        // Store AI response
        memory.add(response.aiMessage());

        return response.aiMessage().text();
    }

    public void clearHistory(String userId) {
        ChatMemory memory = memoryProvider.get(userId);
        memory.clear();
    }
}

Token-Based Memory Window

/**
 * Memory that enforces token limits instead of message count
 */
public class TokenWindowChatMemory implements ChatMemory {
    private final Object id;
    private final int maxTokens;
    private final Tokenizer tokenizer;
    private final List<ChatMessage> messages = new ArrayList<>();

    @Override
    public void add(ChatMessage message) {
        messages.add(message);

        // Calculate total tokens
        int totalTokens = messages.stream()
            .mapToInt(msg -> tokenizer.countTokens(msg.text()))
            .sum();

        // Evict oldest messages until within limit
        while (totalTokens > maxTokens && messages.size() > 1) {
            ChatMessage removed = messages.remove(0);
            totalTokens -= tokenizer.countTokens(removed.text());
        }
    }

    @Override
    public List<ChatMessage> messages() {
        return new ArrayList<>(messages);
    }
}

Summarizing Memory

/**
 * Memory that summarizes old messages to save context
 */
public class SummarizingChatMemory implements ChatMemory {
    private final ChatModel chatModel;
    private final int maxRecentMessages;
    private final List<ChatMessage> messages = new ArrayList<>();
    private String summary = "";

    public SummarizingChatMemory(ChatModel chatModel, int maxRecentMessages) {
        this.chatModel = chatModel;
        this.maxRecentMessages = maxRecentMessages;
    }

    @Override
    public void add(ChatMessage message) {
        messages.add(message);

        // When we have too many messages, summarize old ones
        if (messages.size() > maxRecentMessages) {
            summarizeOldMessages();
        }
    }

    @Override
    public List<ChatMessage> messages() {
        List<ChatMessage> result = new ArrayList<>();

        // Add summary as system message if exists
        if (!summary.isEmpty()) {
            result.add(SystemMessage.from("Previous conversation summary: " + summary));
        }

        // Add recent messages
        result.addAll(messages);

        return result;
    }

    private void summarizeOldMessages() {
        // Get messages to summarize (keep last maxRecentMessages)
        int toSummarize = messages.size() - maxRecentMessages;
        List<ChatMessage> oldMessages = messages.subList(0, toSummarize);

        // Generate summary
        String prompt = String.format("""
            Summarize this conversation concisely:

            %s

            Summary:
            """, formatMessages(oldMessages));

        String newSummary = chatModel.chat(prompt);

        // Update summary
        if (summary.isEmpty()) {
            summary = newSummary;
        } else {
            summary = summary + "\n\n" + newSummary;
        }

        // Remove summarized messages
        messages.subList(0, toSummarize).clear();
    }

    private String formatMessages(List<ChatMessage> messages) {
        return messages.stream()
            .map(msg -> msg.type() + ": " + msg.text())
            .collect(Collectors.joining("\n"));
    }
}

Persistent Storage Examples

Redis Storage

import redis.clients.jedis.JedisPool;
import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * Redis-backed chat memory store
 */
public class RedisChatMemoryStore implements ChatMemoryStore {
    private final JedisPool jedisPool;
    private final ObjectMapper objectMapper;

    public RedisChatMemoryStore(JedisPool jedisPool) {
        this.jedisPool = jedisPool;
        this.objectMapper = new ObjectMapper();
    }

    @Override
    public List<ChatMessage> getMessages(Object memoryId) {
        try (var jedis = jedisPool.getResource()) {
            String json = jedis.get("chat:memory:" + memoryId);
            if (json == null) {
                return new ArrayList<>();
            }
            return objectMapper.readValue(json, new TypeReference<List<ChatMessage>>() {});
        } catch (Exception e) {
            throw new RuntimeException("Failed to get messages", e);
        }
    }

    @Override
    public void updateMessages(Object memoryId, List<ChatMessage> messages) {
        try (var jedis = jedisPool.getResource()) {
            String json = objectMapper.writeValueAsString(messages);
            jedis.setex("chat:memory:" + memoryId, 86400, json);  // 24 hour TTL
        } catch (Exception e) {
            throw new RuntimeException("Failed to update messages", e);
        }
    }

    @Override
    public void deleteMessages(Object memoryId) {
        try (var jedis = jedisPool.getResource()) {
            jedis.del("chat:memory:" + memoryId);
        }
    }
}

Database Storage

import javax.sql.DataSource;
import java.sql.*;

/**
 * SQL database-backed chat memory store
 */
public class DatabaseChatMemoryStore implements ChatMemoryStore {
    private final DataSource dataSource;
    private final ObjectMapper objectMapper;

    public DatabaseChatMemoryStore(DataSource dataSource) {
        this.dataSource = dataSource;
        this.objectMapper = new ObjectMapper();
    }

    @Override
    public List<ChatMessage> getMessages(Object memoryId) {
        String sql = "SELECT messages FROM chat_memory WHERE memory_id = ?";

        try (Connection conn = dataSource.getConnection();
             PreparedStatement stmt = conn.prepareStatement(sql)) {

            stmt.setString(1, memoryId.toString());
            ResultSet rs = stmt.executeQuery();

            if (rs.next()) {
                String json = rs.getString("messages");
                return objectMapper.readValue(json, new TypeReference<List<ChatMessage>>() {});
            }

            return new ArrayList<>();

        } catch (Exception e) {
            throw new RuntimeException("Failed to get messages", e);
        }
    }

    @Override
    public void updateMessages(Object memoryId, List<ChatMessage> messages) {
        String sql = """
            INSERT INTO chat_memory (memory_id, messages, updated_at)
            VALUES (?, ?, ?)
            ON CONFLICT (memory_id)
            DO UPDATE SET messages = EXCLUDED.messages, updated_at = EXCLUDED.updated_at
            """;

        try (Connection conn = dataSource.getConnection();
             PreparedStatement stmt = conn.prepareStatement(sql)) {

            String json = objectMapper.writeValueAsString(messages);

            stmt.setString(1, memoryId.toString());
            stmt.setString(2, json);
            stmt.setTimestamp(3, new Timestamp(System.currentTimeMillis()));

            stmt.executeUpdate();

        } catch (Exception e) {
            throw new RuntimeException("Failed to update messages", e);
        }
    }

    @Override
    public void deleteMessages(Object memoryId) {
        String sql = "DELETE FROM chat_memory WHERE memory_id = ?";

        try (Connection conn = dataSource.getConnection();
             PreparedStatement stmt = conn.prepareStatement(sql)) {

            stmt.setString(1, memoryId.toString());
            stmt.executeUpdate();

        } catch (Exception e) {
            throw new RuntimeException("Failed to delete messages", e);
        }
    }
}

Best Practices

1. Enforce Memory Limits

// ✅ GOOD: Limit message count or tokens
ChatMemory memory = MessageWindowChatMemory.withMaxMessages(20);

// ❌ BAD: Unlimited growth
// Will eventually exceed context window or cause OOM

2. Always Include System Message

// ✅ GOOD: System message for consistent behavior
if (memory.messages().isEmpty()) {
    memory.add(SystemMessage.from("You are a helpful assistant specializing in Java."));
}

3. Thread-Safety for Multi-User

// ✅ GOOD: Synchronize access per user
private final Map<String, Object> userLocks = new ConcurrentHashMap<>();

public String chat(String userId, String message) {
    Object lock = userLocks.computeIfAbsent(userId, k -> new Object());

    synchronized (lock) {
        ChatMemory memory = memoryProvider.get(userId);
        // ... rest of chat logic
    }
}

// ❌ BAD: No synchronization
// Race conditions in multi-threaded environments

4. Set TTL for Persistent Storage

// ✅ GOOD: Expire old conversations
jedis.setex("chat:memory:" + userId, 86400, json);  // 24 hours

// ❌ BAD: Never expire
// Memory/storage grows indefinitely

5. Handle Context Window Overflow

// ✅ GOOD: Summarize or truncate when approaching limit
if (totalTokens > MAX_CONTEXT_TOKENS * 0.8) {
    summarizeOldMessages();
}

Memory Strategies Comparison

StrategyProsConsBest For
Message WindowSimple, predictableMay lose important contextShort conversations
Token WindowBetter context utilizationRequires tokenizerCost-conscious apps
SummarizationPreserves key informationLossy, requires LLM callsLong conversations
No MemoryStateless, simpleNo contextSingle-turn Q&A

Common Pitfalls

PitfallSolution
No memory limitsUse MessageWindowChatMemory or TokenWindowChatMemory
Not thread-safeSynchronize access per user
Missing system messageAlways ensure system message exists
No TTL in storageSet expiration for old conversations
Exceeding context windowMonitor token count, summarize or truncate

Testing

@Test
public void testChatMemory() {
    ChatMemory memory = MessageWindowChatMemory.withMaxMessages(3);

    // Add messages
    memory.add(UserMessage.from("Message 1"));
    memory.add(AiMessage.from("Response 1"));
    memory.add(UserMessage.from("Message 2"));
    memory.add(AiMessage.from("Response 2"));

    // Should have 3 messages (oldest evicted)
    assertEquals(3, memory.messages().size());

    // Clear
    memory.clear();
    assertTrue(memory.messages().isEmpty());
}

See Also

  • Chat Models - Using memory with chat models
  • Chat Messages - Message types stored in memory
  • Guardrails - Context window guardrails

Install with Tessl CLI

npx tessl i tessl/maven-dev-langchain4j--langchain4j-core

docs

guardrails.md

index.md

memory.md

observability.md

tools.md

tile.json