Core classes and interfaces of LangChain4j providing foundational abstractions for LLM interaction, RAG, embeddings, agents, and observability
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.
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();
}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);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);
}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);
}
}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);
}/**
* 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();
}
}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();
}
}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();
}
}/**
* 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);
}
}/**
* 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"));
}
}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);
}
}
}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);
}
}
}// ✅ GOOD: Limit message count or tokens
ChatMemory memory = MessageWindowChatMemory.withMaxMessages(20);
// ❌ BAD: Unlimited growth
// Will eventually exceed context window or cause OOM// ✅ GOOD: System message for consistent behavior
if (memory.messages().isEmpty()) {
memory.add(SystemMessage.from("You are a helpful assistant specializing in Java."));
}// ✅ 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// ✅ GOOD: Expire old conversations
jedis.setex("chat:memory:" + userId, 86400, json); // 24 hours
// ❌ BAD: Never expire
// Memory/storage grows indefinitely// ✅ GOOD: Summarize or truncate when approaching limit
if (totalTokens > MAX_CONTEXT_TOKENS * 0.8) {
summarizeOldMessages();
}| Strategy | Pros | Cons | Best For |
|---|---|---|---|
| Message Window | Simple, predictable | May lose important context | Short conversations |
| Token Window | Better context utilization | Requires tokenizer | Cost-conscious apps |
| Summarization | Preserves key information | Lossy, requires LLM calls | Long conversations |
| No Memory | Stateless, simple | No context | Single-turn Q&A |
| Pitfall | Solution |
|---|---|
| No memory limits | Use MessageWindowChatMemory or TokenWindowChatMemory |
| Not thread-safe | Synchronize access per user |
| Missing system message | Always ensure system message exists |
| No TTL in storage | Set expiration for old conversations |
| Exceeding context window | Monitor token count, summarize or truncate |
@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());
}Install with Tessl CLI
npx tessl i tessl/maven-dev-langchain4j--langchain4j-core