CtrlK
CommunityDocumentationLog inGet started
Tessl Logo

tessl/maven-org-springframework-ai--spring-ai-autoconfigure-model-chat-memory

Spring Boot auto-configuration for chat memory functionality in Spring AI applications

Overview
Eval results
Files

edge-cases.mddocs/examples/

Edge Cases and Error Handling

Advanced scenarios, error handling patterns, and edge case management.

Input Validation Edge Cases

Null and Empty Conversation IDs

@Service
public class SafeChatMemoryService {

    @Autowired
    private ChatMemory chatMemory;
    
    private static final Logger logger = LoggerFactory.getLogger(SafeChatMemoryService.class);

    public boolean addMessageSafely(String conversationId, Message message) {
        try {
            // Validate conversation ID
            if (conversationId == null) {
                logger.error("Conversation ID cannot be null");
                return false;
            }
            
            if (conversationId.trim().isEmpty()) {
                logger.error("Conversation ID cannot be empty");
                return false;
            }
            
            // Validate message
            if (message == null) {
                logger.error("Message cannot be null");
                return false;
            }
            
            chatMemory.add(conversationId, message);
            return true;
            
        } catch (IllegalArgumentException e) {
            logger.error("Validation error: {}", e.getMessage());
            return false;
        } catch (Exception e) {
            logger.error("Unexpected error adding message", e);
            return false;
        }
    }
}

Null Messages in List

public void addMessagesBatch(String conversationId, List<Message> messages) {
    if (messages == null || messages.isEmpty()) {
        logger.warn("No messages to add for conversation: {}", conversationId);
        return;
    }
    
    // Check for null messages in the list
    if (messages.stream().anyMatch(Objects::isNull)) {
        throw new IllegalArgumentException(
            "Message list cannot contain null elements"
        );
    }
    
    try {
        chatMemory.add(conversationId, messages);
    } catch (IllegalArgumentException e) {
        logger.error("Failed to add messages batch: {}", e.getMessage());
        throw e;
    }
}

Concurrent Access Edge Cases

Race Conditions

@Service
public class ConcurrentSafeChatService {

    @Autowired
    private ChatMemory chatMemory;
    
    private final ConcurrentHashMap<String, ReentrantLock> conversationLocks = 
        new ConcurrentHashMap<>();

    public void addMessageWithLock(String conversationId, Message message) {
        ReentrantLock lock = conversationLocks.computeIfAbsent(
            conversationId, 
            k -> new ReentrantLock()
        );
        
        lock.lock();
        try {
            chatMemory.add(conversationId, message);
        } finally {
            lock.unlock();
        }
    }
    
    public List<Message> getMessagesWithLock(String conversationId) {
        ReentrantLock lock = conversationLocks.computeIfAbsent(
            conversationId,
            k -> new ReentrantLock()
        );
        
        lock.lock();
        try {
            // Return a defensive copy
            return new ArrayList<>(chatMemory.get(conversationId));
        } finally {
            lock.unlock();
        }
    }
}

Parallel Message Processing

@Service
public class ParallelMessageProcessor {

    @Autowired
    private ChatMemory chatMemory;

    public void processMessagesInParallel(Map<String, String> conversationMessages) {
        // ChatMemory implementations should be thread-safe
        conversationMessages.entrySet().parallelStream()
            .forEach(entry -> {
                try {
                    chatMemory.add(
                        entry.getKey(),
                        new UserMessage(entry.getValue())
                    );
                } catch (Exception e) {
                    logger.error("Failed to process message for {}: {}", 
                        entry.getKey(), e.getMessage());
                }
            });
    }
}

Repository Failure Handling

Database Connection Failures

@Service
public class ResilientChatMemoryService {

    @Autowired
    private ChatMemory chatMemory;
    
    @Autowired
    private ChatMemoryRepository repository;
    
    private final RetryTemplate retryTemplate;
    
    public ResilientChatMemoryService() {
        this.retryTemplate = RetryTemplate.builder()
            .maxAttempts(3)
            .fixedBackoff(1000)
            .retryOn(DataAccessException.class)
            .build();
    }

    public void addMessageWithRetry(String conversationId, Message message) {
        retryTemplate.execute(context -> {
            chatMemory.add(conversationId, message);
            return null;
        });
    }
    
    public List<Message> getMessagesWithFallback(String conversationId) {
        try {
            return chatMemory.get(conversationId);
        } catch (DataAccessException e) {
            logger.error("Database error retrieving messages, returning empty list", e);
            return Collections.emptyList();
        }
    }
    
    public boolean isRepositoryHealthy() {
        try {
            repository.findConversationIds();
            return true;
        } catch (Exception e) {
            logger.error("Repository health check failed", e);
            return false;
        }
    }
}

Transaction Rollback Scenarios

@Service
public class TransactionalChatService {

    @Autowired
    private ChatMemory chatMemory;
    
    @Autowired
    private ExternalService externalService;

    @Transactional(rollbackFor = Exception.class)
    public void addMessageWithExternalCall(String conversationId, Message message) {
        try {
            // Add message to memory
            chatMemory.add(conversationId, message);
            
            // Call external service
            externalService.notifyNewMessage(conversationId, message);
            
        } catch (Exception e) {
            logger.error("Transaction failed, rolling back", e);
            throw e; // Rollback transaction
        }
    }
    
    @Transactional(rollbackFor = Exception.class)
    public void addMessagesAtomic(String conversationId, List<Message> messages) {
        // All messages added or none (atomic operation)
        chatMemory.add(conversationId, messages);
    }
}

Memory Management Edge Cases

Message Window Overflow

@Service
public class MessageWindowManager {

    @Autowired
    private ChatMemory chatMemory;
    
    private final int maxMessages = 20; // Default window size

    public void addMessageWithOverflowWarning(String conversationId, Message message) {
        List<Message> currentMessages = chatMemory.get(conversationId);
        
        if (currentMessages.size() >= maxMessages) {
            logger.warn("Conversation {} at capacity ({} messages), oldest will be evicted",
                conversationId, maxMessages);
        }
        
        chatMemory.add(conversationId, message);
    }
    
    public List<Message> getMessagesWithSystemPreservation(String conversationId) {
        List<Message> messages = chatMemory.get(conversationId);
        
        // System messages should be preserved
        long systemMessageCount = messages.stream()
            .filter(m -> m.getMessageType() == MessageType.SYSTEM)
            .count();
        
        if (systemMessageCount == 0) {
            logger.warn("No system message found in conversation: {}", conversationId);
        }
        
        return messages;
    }
}

Large Message Handling

@Service
public class LargeMessageHandler {

    @Autowired
    private ChatMemory chatMemory;
    
    private static final int MAX_MESSAGE_SIZE = 10000; // characters

    public void addLargeMessage(String conversationId, String content) {
        if (content.length() > MAX_MESSAGE_SIZE) {
            logger.warn("Message exceeds size limit ({} chars), truncating",
                content.length());
            
            // Option 1: Truncate
            String truncated = content.substring(0, MAX_MESSAGE_SIZE) + "... [truncated]";
            chatMemory.add(conversationId, new UserMessage(truncated));
            
            // Option 2: Split into multiple messages
            // splitAndAddMessages(conversationId, content);
        } else {
            chatMemory.add(conversationId, new UserMessage(content));
        }
    }
    
    private void splitAndAddMessages(String conversationId, String content) {
        List<Message> messages = new ArrayList<>();
        int start = 0;
        int part = 1;
        
        while (start < content.length()) {
            int end = Math.min(start + MAX_MESSAGE_SIZE, content.length());
            String chunk = content.substring(start, end);
            
            messages.add(new UserMessage(
                String.format("[Part %d] %s", part++, chunk)
            ));
            
            start = end;
        }
        
        chatMemory.add(conversationId, messages);
    }
}

System Message Edge Cases

Multiple System Messages

@Service
public class SystemMessageManager {

    @Autowired
    private ChatMemory chatMemory;

    public void updateSystemMessage(String conversationId, String newSystemPrompt) {
        // MessageWindowChatMemory replaces old system messages
        SystemMessage newSystemMessage = new SystemMessage(newSystemPrompt);
        chatMemory.add(conversationId, newSystemMessage);
        
        // Verify only one system message exists
        List<Message> messages = chatMemory.get(conversationId);
        long systemMessageCount = messages.stream()
            .filter(m -> m.getMessageType() == MessageType.SYSTEM)
            .count();
        
        if (systemMessageCount > 1) {
            logger.warn("Multiple system messages found in conversation: {}",
                conversationId);
        }
    }
    
    public void ensureSystemMessage(String conversationId, String defaultPrompt) {
        List<Message> messages = chatMemory.get(conversationId);
        
        boolean hasSystemMessage = messages.stream()
            .anyMatch(m -> m.getMessageType() == MessageType.SYSTEM);
        
        if (!hasSystemMessage) {
            logger.info("Adding default system message to conversation: {}",
                conversationId);
            chatMemory.add(conversationId, new SystemMessage(defaultPrompt));
        }
    }
}

Conversation ID Edge Cases

Special Characters in IDs

@Service
public class ConversationIdValidator {

    private static final Pattern VALID_ID_PATTERN = 
        Pattern.compile("^[a-zA-Z0-9:_-]+$");

    public String sanitizeConversationId(String rawId) {
        if (rawId == null || rawId.trim().isEmpty()) {
            throw new IllegalArgumentException("Conversation ID cannot be null or empty");
        }
        
        // Remove invalid characters
        String sanitized = rawId.replaceAll("[^a-zA-Z0-9:_-]", "_");
        
        if (!sanitized.equals(rawId)) {
            logger.warn("Sanitized conversation ID from '{}' to '{}'", rawId, sanitized);
        }
        
        return sanitized;
    }
    
    public boolean isValidConversationId(String conversationId) {
        return conversationId != null && 
               !conversationId.trim().isEmpty() &&
               VALID_ID_PATTERN.matcher(conversationId).matches();
    }
}

Collision Prevention

@Service
public class UniqueConversationIdGenerator {

    private final AtomicLong counter = new AtomicLong(0);

    public String generateUniqueId(String prefix) {
        return String.format("%s:%d:%d", 
            prefix,
            System.currentTimeMillis(),
            counter.incrementAndGet()
        );
    }
    
    public String generateUserConversationId(String userId) {
        // Ensure uniqueness with timestamp
        return String.format("user:%s:%d", 
            userId,
            System.currentTimeMillis()
        );
    }
}

Media and Tool Message Edge Cases

Media Message Handling

@Service
public class MediaMessageHandler {

    @Autowired
    private ChatMemory chatMemory;

    public void addMessageWithMedia(String conversationId, String text, byte[] imageData) {
        try {
            Media media = Media.builder()
                .mimeType(Media.Format.IMAGE_PNG)
                .data(imageData)
                .name("image.png")
                .build();
            
            UserMessage message = UserMessage.builder()
                .text(text)
                .media(media)
                .build();
            
            chatMemory.add(conversationId, message);
            
        } catch (Exception e) {
            logger.error("Failed to add media message", e);
            // Fallback: add text-only message
            chatMemory.add(conversationId, new UserMessage(text));
        }
    }
    
    public void handleLargeMedia(String conversationId, String text, byte[] mediaData) {
        if (mediaData.length > 10_000_000) { // 10MB
            logger.warn("Media size exceeds limit: {} bytes", mediaData.length);
            // Store media externally and add reference
            String mediaUrl = storeMediaExternally(mediaData);
            chatMemory.add(conversationId, 
                new UserMessage(text + "\n[Media: " + mediaUrl + "]"));
        } else {
            addMessageWithMedia(conversationId, text, mediaData);
        }
    }
}

Tool Response Handling

@Service
public class ToolResponseHandler {

    @Autowired
    private ChatMemory chatMemory;

    public void addToolResponse(String conversationId, String toolCallId, 
                                String toolName, String responseData) {
        try {
            ToolResponseMessage.ToolResponse response = 
                new ToolResponseMessage.ToolResponse(toolCallId, toolName, responseData);
            
            ToolResponseMessage message = ToolResponseMessage.builder()
                .responses(List.of(response))
                .build();
            
            chatMemory.add(conversationId, message);
            
        } catch (Exception e) {
            logger.error("Failed to add tool response", e);
        }
    }
    
    public void addMultipleToolResponses(String conversationId, 
                                        List<ToolResponseMessage.ToolResponse> responses) {
        if (responses == null || responses.isEmpty()) {
            logger.warn("No tool responses to add");
            return;
        }
        
        try {
            ToolResponseMessage message = ToolResponseMessage.builder()
                .responses(responses)
                .build();
            
            chatMemory.add(conversationId, message);
            
        } catch (IllegalArgumentException e) {
            logger.error("Invalid tool responses: {}", e.getMessage());
        }
    }
}

Repository-Specific Edge Cases

JDBC Schema Initialization Failures

@Service
public class JdbcSchemaValidator {

    @Autowired
    private DataSource dataSource;

    public boolean validateSchema() {
        try (Connection conn = dataSource.getConnection()) {
            DatabaseMetaData metaData = conn.getMetaData();
            
            // Check if chat memory table exists
            try (ResultSet rs = metaData.getTables(null, null, "chat_memory", null)) {
                if (!rs.next()) {
                    logger.error("Chat memory table does not exist");
                    return false;
                }
            }
            
            return true;
            
        } catch (SQLException e) {
            logger.error("Failed to validate schema", e);
            return false;
        }
    }
    
    public void initializeSchemaManually() {
        try (Connection conn = dataSource.getConnection();
             Statement stmt = conn.createStatement()) {
            
            String createTable = """
                CREATE TABLE IF NOT EXISTS chat_memory (
                    conversation_id VARCHAR(255) NOT NULL,
                    message_id VARCHAR(255) NOT NULL,
                    message_type VARCHAR(50) NOT NULL,
                    content TEXT,
                    metadata TEXT,
                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                    PRIMARY KEY (conversation_id, message_id)
                )
                """;
            
            stmt.execute(createTable);
            logger.info("Schema initialized successfully");
            
        } catch (SQLException e) {
            logger.error("Failed to initialize schema", e);
            throw new RuntimeException("Schema initialization failed", e);
        }
    }
}

MongoDB Connection Failures

@Service
public class MongoHealthChecker {

    @Autowired
    private MongoTemplate mongoTemplate;

    public boolean isMongoAvailable() {
        try {
            mongoTemplate.executeCommand("{ ping: 1 }");
            return true;
        } catch (Exception e) {
            logger.error("MongoDB not available", e);
            return false;
        }
    }
    
    public void ensureIndexes() {
        try {
            IndexOperations indexOps = mongoTemplate.indexOps("chat_memory");
            
            // Create conversation_id index
            Index conversationIndex = new Index()
                .on("conversationId", Sort.Direction.ASC);
            indexOps.ensureIndex(conversationIndex);
            
            logger.info("MongoDB indexes ensured");
            
        } catch (Exception e) {
            logger.error("Failed to create indexes", e);
        }
    }
}

Cleanup and Maintenance Edge Cases

Orphaned Conversations

@Service
public class ConversationCleanupService {

    @Autowired
    private ChatMemoryRepository repository;

    public void cleanupEmptyConversations() {
        List<String> conversationIds = repository.findConversationIds();
        
        conversationIds.forEach(id -> {
            List<Message> messages = repository.findByConversationId(id);
            
            if (messages.isEmpty()) {
                logger.info("Deleting empty conversation: {}", id);
                repository.deleteByConversationId(id);
            }
        });
    }
    
    public void cleanupOldConversations(Duration maxAge) {
        List<String> conversationIds = repository.findConversationIds();
        Instant cutoff = Instant.now().minus(maxAge);
        
        conversationIds.forEach(id -> {
            if (isOlderThan(id, cutoff)) {
                logger.info("Deleting old conversation: {}", id);
                repository.deleteByConversationId(id);
            }
        });
    }
    
    private boolean isOlderThan(String conversationId, Instant cutoff) {
        List<Message> messages = repository.findByConversationId(conversationId);
        if (messages.isEmpty()) return true;
        
        // Check last message timestamp
        Message lastMessage = messages.get(messages.size() - 1);
        Object timestamp = lastMessage.getMetadata().get("timestamp");
        
        return timestamp instanceof Instant && 
               ((Instant) timestamp).isBefore(cutoff);
    }
}

Error Recovery Patterns

Circuit Breaker Pattern

@Service
public class CircuitBreakerChatService {

    @Autowired
    private ChatMemory chatMemory;
    
    private final CircuitBreaker circuitBreaker;
    
    public CircuitBreakerChatService() {
        CircuitBreakerConfig config = CircuitBreakerConfig.custom()
            .failureRateThreshold(50)
            .waitDurationInOpenState(Duration.ofSeconds(30))
            .slidingWindowSize(10)
            .build();
        
        this.circuitBreaker = CircuitBreaker.of("chatMemory", config);
    }

    public void addMessageWithCircuitBreaker(String conversationId, Message message) {
        Supplier<Void> supplier = () -> {
            chatMemory.add(conversationId, message);
            return null;
        };
        
        Try.ofSupplier(CircuitBreaker.decorateSupplier(circuitBreaker, supplier))
            .onFailure(e -> logger.error("Circuit breaker prevented call", e));
    }
}

Best Practices Summary

  1. Always Validate Inputs: Check for null, empty, and invalid values
  2. Handle Exceptions Gracefully: Use try-catch and provide fallbacks
  3. Implement Retry Logic: For transient failures
  4. Use Circuit Breakers: For repeated failures
  5. Monitor Repository Health: Regular health checks
  6. Implement Cleanup: Remove orphaned and old conversations
  7. Handle Concurrent Access: Use locks when necessary
  8. Validate Message Size: Prevent memory issues
  9. Test Edge Cases: Unit tests for all edge cases
  10. Log Appropriately: Debug, warn, and error logs

Next Steps

  • Real-World Scenarios - Production use cases
  • Troubleshooting Reference - Common issues
  • Performance Reference - Optimization tips
tessl i tessl/maven-org-springframework-ai--spring-ai-autoconfigure-model-chat-memory@1.1.0

docs

examples

edge-cases.md

real-world-scenarios.md

index.md

tile.json