Transaction Management in Spring Framework

Emre Altun
8 min readJun 20, 2021

In this article, I am going to give some information about transaction management in the spring framework, Before I start, I would like to touch on the keywords that we will mention a lot.

Transaction Management in Spring Framework

What are Commit and Rollback ?

When we have made any data manipulation, the databases are waiting for our approval in the “UPDATE” and “INSERT” transactions. The word we use to confirm the transactions is called “COMMIT” in the SQL language. Thanks to this word, we say that we want to confirm our insert or update operations. However, if we do not want the approve our transactions to be recorded in the database, then we provide cancellation of our transactions by using the word “ROLLBACK”.

What is Transaction?

The transaction is a process of managing one or more database manipulation together. It provides functionality for committing all changes at the same time or reverting all changes at the same time. Thanks to this data consistency are provided for modern applications.

Transaction Management

There are two types of transaction management that are pragmatic method and declarative method.

Pragmatic method

It is a method that contains the codes of starting and ending the transaction process and closing the open connections.

@Test
void createUser(UserDto userDetails) {
// transaction definition
TransactionStatus status = transactionManager.getTransaction(definition);
try {
userDetails.setUserId(UUID.randomUUID().toString());
userDetails.setEncryptedPassword(bCryptPasswordEncoder.encode(userDetails.getPassword()));
ModelMapper modelMapper = new ModelMapper();
modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
UserEntity userEntity = modelMapper.map(userDetails, UserEntity.class);

entityManager.persist(userEntity);
transactionManager.commit(status);
} catch (Exception ex) {
transactionManager.rollback(status);
}
assertThat(entityManager.createQuery("select u from User u").getResultList()).hasSize(1);
}

Declarative method

In Spring Framework, this can manage transactions by using Spring Container.

It’s done thanks to the @Transactional Annotation

  1. It is performed at the class or method level. The animation you wrote at the class level includes all the public methods in the class.
  2. A new transaction is initiated by the Spring Container when the Service Method is called. if The method is successfully completed, the changes are committed. But if any exception occurred during the method execution, all changes are reverted by the transectiıon.
@Transactional(propagation = Propagation.REQUIRED)
public AsenkronRaporBilgi create(String raporAdi, RaporTuru raporTuru, KnowsQueryCriteriaHolderClass sorguKriteriClass, SessionUtils sessionInfo) {
AsenkronRaporBilgi asenkronRaporBilgi = new AsenkronRaporBilgi();
asenkronRaporBilgi.initialize(raporAdi, sessionInfo, raporTuru);
asenkronRaporBilgi.setSerializedFields(sorguKriteriClass, sessionInfo);
return asenkronRaporBilgiRepository.save(asenkronRaporBilgi);
}

Spring Boot Transaction Features

  1. Propagation
  2. ReadOnly
  3. TimeOut
  4. rollbackFor
  5. Isolation

Propagation

There are 7 different Propagation types. When the service method is called, it is an enum class that determines whether the Transaction will be started or not, if there is an existing transaction, or whether it will continue or a new transaction is created.

  1. REQUIRED

This is a Default propagation type. If there is no active Transaction, it opens a new transaction. If there is an active Transaction, it participates in it.

@Transactional(propagation = Propagation.REQUIRED)
public Belge saveBelge(Sirket sirket, String fileName, String minetype, EBelgeTipi belgeTipi, byte[] excelBytes) {
Belge belge = Belge.builder().build();
belge.initialize(sirket, fileName, minetype, belgeTipi, excelBytes);
return belgeRepository.save(belge);
}

2. REQUIRES_NEW

If there is an active Transaction, it waits for the active one and opens a new transaction. Then When the required_new one is completed successfully, this committed the changes and continues the first Transaction. If a method that is marked as REQUIRED_NEW transaction is the first method, it behaves as REQUIRED one.

This is the first method that is marked as REQUIRED.

@Transactional(propagation = Propagation.REQUIRED)
public void prepareReport(AsenkronRaporBilgi asenkronRaporBilgi) {
// eğer başka bir thread tarafından başlatıldı ise durması sağlanıyor.
Boolean canStart = asenkronRaporGeneratorService.canStart(asenkronRaporBilgi);
if (!canStart) return;

// başlatılır.
asenkronRaporBilgi = asenkronRaporGeneratorService.start(asenkronRaporBilgi);

// excel generate ediliyor.
byte[] excelBytes = createExcelBytes(asenkronRaporBilgi);

// süreç sonlşandırılıyor.
asenkronRaporGeneratorService.finish(asenkronRaporBilgi, excelBytes);
}

This is the second method that is marked as REQUIRED_NEW. Spring Container creates a new Transaction.

@Transactional(propagation = Propagation.REQUIRES_NEW)
public AsenkronRaporBilgi start(AsenkronRaporBilgi raporBilgi) {
raporBilgi.startProcess();
return asenkronRaporBilgiRepository.save(raporBilgi);
}

3. NESTED

This is a valid rule in JDBC. It is using the savepoint technology that comes with JDBC 3.0. By using this, we define a savepoint that is the point which we want to roll back the changes until this point state.

When any exception occurs, data manipulations are reverted until this savepoint and the rest of the method continue to execute.

This is the first method that is marked as NESTED.

@Override
@Transactional(propagation = Propagation.REQUIRED)
public void syncCariKart(CariKartViewHolder cariKartViewHolder) {
List<CariKartVisitor> cariKartVisitorList = cariVisitorFactory.getCariVisitorList();
cariKartVisitorList.forEach(visitor -> {
visitor.visit(cariKartViewHolder);
});
}

This is the second method that is marked as NESTED. Spring Container set a savepoint and if there is any exception occured, only second method’s changes are reverted but all changes are commited after first method completed successfully.

@Override
@Transactional(propagation = Propagation.NESTED)
public void visit(CariKartViewHolder cariKartViewHolder) {
// cariKart bulunur
CariKart cariKart = cariKartRepository.findByCariKodu(cariKartViewHolder.getHesapKodu());
final List<Siparis> oldSiparisList = siparisRepository.findAllByCariKart(cariKart);
// siparis kayitlarini sileriz.
siparisRepository.deleteAll(oldSiparisList);
// siparis üzerinde giziniriz
List<Siparis> siparisList = new ArrayList<>();
if (!CollectionUtils.isEmpty(cariKartViewHolder.getSiparisList())) {
cariKartViewHolder.getSiparisList().forEach(siparisViewHolder -> {
siparisList.add(saveSiparis(cariKart, siparisViewHolder));
});
siparisRepository.saveAll(siparisList);
}
}

4. SUPPORTS

If there is Transaction in the service method, it will run, otherwise, it will run without Transaction.

@Override
@Transactional(propagation = Propagation.SUPPORTS)
public Long currentNumberOfSales() {
return faturaRepository.numberOfSales(DateUtils.getToday(), EDurum.AKTIF, EOdemeYonu.BORC);
}

5. NOT_SUPPORTED

If there is any active Transaction, it is suspended and the service method is executed without Transaction. After the method has been completed, the container resumes the client’s transaction.

It is recommended to use the NotSupported attribute for methods that don’t need transactions. Because transactions involve overhead, this attribute may improve performance.

@Transactional(propagation = Propagation.NOT_SUPPORTED)
public BigDecimal getIskontoOrani() {
return getTutar().compareTo(BigDecimal.ZERO) > 0 ? (getIskonto().divide(getTutar(), 2, RoundingMode.HALF_UP).multiply(BigDecimal.valueOf(100))) : BigDecimal.valueOf(0);
}

6. NEVER

For transactional logic with NEVER propagation, Spring throws an exception if there’s an active transaction:

@Transactional(propagation = Propagation.NEVER)
public BigDecimal getIskontoOrani() {
return getTutar().compareTo(BigDecimal.ZERO) > 0 ? (getIskonto().divide(getTutar(), 2, RoundingMode.HALF_UP).multiply(BigDecimal.valueOf(100))) : BigDecimal.valueOf(0);
}

7. MANDATORY

If there is an active transaction, then it will be used. If there isn’t an active transaction, then Spring throws an exception:

@Override
@Transactional(propagation = Propagation.MANDATORY)
public RoleInput updateRole(Long id, RoleInput input) {
Role role = roleRepository.findById(id).get();
roleMapper.updateFromInput(input, role);
return roleMapper.toInput(roleRepository.save(role));
}

ReadOnly

When this property is set to true, a read-only transaction is opened. It can be used in transactions where no changes will be made to the database.

@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
public Long findBelgeById(Long id) {
return asenkronRaporBilgiRepository.findBelgeById(id);
}

Timeout

Thanks to this defining feature, when the method is not completed within the specified time, it performs the rollback operation.

@Transactional(timeout = 10800,propagation = Propagation.SUPPORTS)
public List sorguSonucuGetir(AsenkronRaporBilgi asenkronRaporBilgi) {
RaporSorguKriteri sorguKriteri = (RaporSorguKriteri) createSorguKriteri(asenkronRaporBilgi);
return reportRepository.cariCiroDonemList(sorguKriteri.getDonem(), sorguKriteri.getYil(), EDurum.AKTIF, EOdemeYonu.BORC, asenkronRaporBilgi.getSirket());
}

rollbackFor

The rollbackFor attribute is used for rollback the transaction for the given exception.

If we don’t provide any rollbackFor Exception or we don’t give rollbackFor as a parameter with @Transnational annotation, spring uses default rollback for RuntimeException/unchecked exception and Error.

When we write a method by using @Transactional annotation.

@Transactional
public void saveUser(UserDto userDto) {
// business logic
}

Spring Framework understand this like that

@Transactional(rollbackFor = { RuntimeException.class, Error.class })
public void saveUser(UserDto userDto) {
// business logic
}

Spring does not provide default rollback for Checked exception and Exception or any custom exception that extends the Exception class.

It is possible to have multiple Exception class values for rollbackFor.

First One:

@Service
public class BelgeServiceImp implements BelgeService {

@Autowired
private BelgeRepository belgeRepository;

@Transactional(propagation = Propagation.REQUIRED)
public Belge saveBelge(Sirket sirket, String fileName, String minetype, EBelgeTipi belgeTipi, byte[] excelBytes) {
Belge belge = Belge.builder().build();
belge.initialize(sirket, fileName, minetype, belgeTipi, excelBytes);
return belgeRepository.save(belge);
}
}

Second One:

@Service
public class BelgeServiceImp implements BelgeService {

@Autowired
private BelgeRepository belgeRepository;

@Transactional(rollbackFor = {RuntimeException.class})
public Belge saveBelge(Sirket sirket, String fileName, String minetype, EBelgeTipi belgeTipi, byte[] excelBytes) {
Belge belge = Belge.builder().build();
belge.initialize(sirket, fileName, minetype, belgeTipi, excelBytes);
return belgeRepository.save(belge);
}
}

Isolation

Insulation is one of the common ACID properties like Atomicity, Consistency, Isolation, and Durability. Isolation describes how changes applied by concurrent processes are visible to each other.

Isolation level is used for preventing the side effects of zero or more concurrency in Transaction.

  • Dirty Read: it is used for reading uncommitted changes of a concurrent Transaction.
  • Nonrepeatable Read: it occurs when a transaction reads the same row twice. and every time it gets a different value. For example, suppose transaction X1 reads data AND there is a concurrency, another transaction X2 updates the same data and commits, if transaction X1 rereads the same data again, it will collect a different value.
  • Phantom read: it occurs when two same queries are executed, the rows that are retrieved by the two queries are different from each other. For example, transaction X1 collects a set of rows that is prepared by some search criteria. Then, Transaction X2 collects some new rows that match the search criteria for transaction X1. If transaction X1 executes the same search criteria again that reads the rows, it gets a different set of rows this time.

the isolation level of a transaction is defining by @Transactional(isolation = Isolation.). There are five types of it in Spring.

  • DEFAULT,
  • READ_UNCOMMITTED,
  • READ_COMMITTED,
  • REPEATABLE_READ,
  • SERIALIZABLE.

READ_COMMITTED Isolation

READ_COMMITTED prevents dirty reads. Dirty reads mean that uncommitted changes in concurrent transactions do not affect the other Transections. However, if a transaction commits its changes, the results could change by re-querying.

@Transactional(isolation = Isolation.READ_COMMITTED)
public void saveBelgeLog(Long belgeId){
// ...
}

READ_COMMITTED is the default level with Postgres, SQL Server, and Oracle.

READ_UNCOMMITTED Isolation

This isolation level allows concurrent access. This means that, while X1 Transaction is executing, if X2 changes any data without finishing the transaction yet, when X1 tries to get data that X2 is changing, it gets the new value of it although X2 does not commit the changes.

We can set the isolation level for a method or class:

@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void saveBelgeLog(Long belgeId){
// ...
}

Not: Postgres and Oracle databases does not support READ_UNCOMMITTED isolation.

REPEATABLE_READ Isolation

REPEATABLE_READ, prevents dirty and non-repeatable reads. Therefore, it is not possible to effect uncommitted changes in concurrent transactions.

When we query for a row more than one time in a single transaction, the method does not return different results. However, When the execution of range-queries or query that is prepared by criteria, we may get newly added or removed rows.

When more than one Transaction trying to access a row and update it, the Lost update may occur. However, REPETABLE_READ does not allow reading a row at the same time. Therefore, it is not possible to face with Lost update.

@Transactional(isolation = Isolation.REPEATABLE_READ)
public void saveBelgeLog(Long belgeId){
// ...
}

This is the default level in Mysql. Oracle does not support it.

SERIALIZABLE Isolation

This is the highest isolation level. It prevents all side effects of concurrent transactions.

@Transactional(isolation = Isolation.SERIALIZABLE)
public void saveBelgeLog(Long belgeId){
// ...
}

To Sum Up: this table is the summary of the things that we talk about isolation levels.

Difference of Isolation levels

s

--

--