spring-projects / spring-projects/spring-ai
`MongoChatMemoryRepository.saveAll()` can lose conversation history on insert failure
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 9.5k
- Forks
- 2.9k
- Avg merge
- 1d 7h
- Merged PRs (30d)
- 6
Description
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
- Persist an initial conversation using MongoChatMemoryRepository.
- Call saveAll() again for the same conversation.
- Make the
MongoTemplate.insert(...)operation fail afterdeleteByConversationId()succeeds. - 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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with saveAll() in MongoChatMemoryRepository and the proposed shouldPreserveExistingMessagesWhenInsertFails() test in MongoChatMemoryRepositoryIT. Compare the existing JdbcChatMemoryRepository transaction flow, then determine the accepted strategy for standalone MongoDB deployments. Done means the failure case preserves prior messages and the repository’s supported deployment behavior is covered by integration tests.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java, mongodb, spring
- Domain
- backend, databases
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100