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 after deleteByConversationId() 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.
Bug Description
MongoChatMemoryRepository.saveAll()updates a conversation by deleting the existing messages and then inserting the new message list: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
MongoTemplate.insert(...)operation fail afterdeleteByConversationId()succeeds.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.
JdbcChatMemoryRepositoryalready provides this behavior by executing the equivalent delete-and-insert flow inside aTransactionTemplate, 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: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
TransactionTemplatebacked by aMongoTransactionManager.Conceptually,
saveAll()would execute both operations within the same transaction: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:
MongoChatMemoryRepository.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.