Few entities were being persisted to database in certain cases, without explicitly calling save() or without having @Transactional annotation. Digging a little deeper, I realised this behaviour is due to persistence context and dirty check mechanism.

Persistence Context

Observation 1: Two entities, user and account, are retrieved from database and fields of both entities are modified. When save() was called only on account entity, both the entities were being persisted to database.

<UserEntity> user = userRepository.findById("83c26890-76f9-4ad8-8a21-e5ed5f097714");
<AccountEntity> account = accountRepository.findById("11c26890-76f9-4ad8-8a21-e5ed5f097714");

user.setAge(25);                                        //modify user entity
account.setType("business_account");                    //modify account entity

accountRepository.save(account);                        //save only account entity

Explanation: JPA’s entityManager has a Level-1 cache called Persistence Context and all managed entities fetched from database are stored into this persistence context. When save() is called on any repository, this persistence context is flushed and all the entities in the persistence context, which has been modified, will be persisted back to database.

Here, when user and account entites are fetched from database, its stored into the persistence context. When accountRepository.save(account) is called, the persistence context is flushed, and since both user and account entites have been modified, they are both persisted back to database with the new changes.

Dirty Check Mechanism

Observation 2: Two entities, user and account, are retrieved from database and only account entity is modified. When save() was called only on account entity, both the entities were being persisted to database. Printing the SQL query generated, showed an update query being run on the user entity as well, even when there was no change for that entity.

<UserEntity> user = userRepository.findById("83c26890-76f9-4ad8-8a21-e5ed5f097714");
<AccountEntity> account = accountRepository.findById("11c26890-76f9-4ad8-8a21-e5ed5f097714");

account.setType("business_account");                    //modify only account entity

accountRepository.save(account);                        //save only account entity

Explanation: Every time a managed entity is loaded into the persistence context, a snapshot/copy of it is kept in memory. When the persistence context is flushed during a save() operation, Hibernate compares the snapshot version of the object with the current version, by calling the .equals() method. If they differ, the object is marked as dirty and an update query is run on that entity.

Here, a field inside UserEntity did not have equals() and hashCode() overridden and hence the snapshot in memory and current version of the object were being treated as different as .equals() does a shallow comparison and returns false. Hence the entity was marked as dirty. Since as per dirty check mechanism, the entity has been modified, an update query was run on userEntity.