Skip to content

MongoChatMemoryRepository.saveAll() can lose conversation history on insert failure #6770

Description

@ricardohsmello

Bug Description

MongoChatMemoryRepository.saveAll() updates a conversation by deleting the existing messages and then inserting the new message list:

deleteByConversationId(conversationId);
this.mongoTemplate.insert(conversations, Conversation.class);

These operations are executed independently, without a transaction.

If the delete succeeds and the subsequent insert fails, the existing messages have already been removed, leaving the conversation without its previously persisted history.

Environment

Spring AI: main (2.0.1-SNAPSHOT)
Module: spring-ai-model-chat-memory-repository-mongodb
MongoDB: 8.x
Spring Data MongoDB / MongoTemplate

Steps to reproduce

  1. Persist an initial conversation using MongoChatMemoryRepository.
  2. Call saveAll() again for the same conversation.
  3. Make the MongoTemplate.insert(...) operation fail after deleteByConversationId() succeeds.
  4. Read the conversation again.

The original conversation is no longer available because the delete operation was completed before the insert failed.

Expected behavior

Updating a persisted conversation should not remove the existing conversation if the replacement messages cannot be saved.

The delete and insert operations should be atomic, so that if the insert fails, the previous conversation remains available.

JdbcChatMemoryRepository already provides this behavior by executing the equivalent delete-and-insert flow inside a TransactionTemplate, allowing the delete to be rolled back when the insert fails.

Minimal Complete Reproducible example

The following test, added to MongoChatMemoryRepositoryIT, reproduces the issue by forcing the insert operation to fail after the existing conversation has already been deleted:

@Test
void shouldPreserveExistingMessagesWhenInsertFails() {
	var conversationId = UUID.randomUUID().toString();
	var existingMessages = List.<Message>of(
			new UserMessage("First message"),
			new AssistantMessage("Second message"));

	this.chatMemoryRepository.saveAll(conversationId, existingMessages);

	var failingMongoTemplate = spy(this.mongoTemplate);
	doThrow(new DataIntegrityViolationException("Insert failed"))
		.when(failingMongoTemplate)
		.insert(anyCollection(), eq(Conversation.class));

	var failingRepository = MongoChatMemoryRepository.builder()
		.mongoTemplate(failingMongoTemplate)
		.build();

	assertThatThrownBy(() ->
			failingRepository.saveAll(conversationId, List.of(new UserMessage("Third message"))))
		.isInstanceOf(DataIntegrityViolationException.class);

	assertThat(this.chatMemoryRepository.findByConversationId(conversationId))
		.isEqualTo(existingMessages);
}

I verified that this test fails against the current main branch and passes with the transactional implementation, where the delete is rolled back when the insert fails.

Possible solution and compatibility consideration

I tested a possible fix locally by wrapping the delete and insert operations in a TransactionTemplate backed by a MongoTransactionManager.

Conceptually, saveAll() would execute both operations within the same transaction:

this.transactionTemplate.executeWithoutResult(status -> {
    deleteByConversationId(conversationId);
    this.mongoTemplate.insert(conversations, Conversation.class);
});

With this change, if the insert fails, the delete is rolled back and the previously persisted conversation remains intact. There is, however, an important compatibility consideration.

MongoDB multi-document transactions are supported on replica sets and sharded clusters, but not on standalone mongod deployments. Making saveAll() transactional unconditionally would therefore break the repository for applications connected to a standalone deployment.

For that reason, I have not opened a PR yet. The remaining question is how Spring AI wants to handle standalone deployments while addressing this consistency issue.

Possible approaches include:

  • Require a transaction-capable MongoDB deployment for MongoChatMemoryRepository.
  • Make transactional behavior configurable.
  • Use transactions when the connected MongoDB topology supports them, while preserving the current behavior for standalone deployments.

I already have a working transactional implementation and an integration test covering the rollback scenario. Once there is agreement on the standalone compatibility strategy, I would be happy to submit a PR.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions