Resilience of Spring Microservice Applications: The Role of @Transactional Annotation in Preventing Connection Leaks
In any microservice, clear management of database interactions is a key factor to maintain application performance and reliability at the required level. We usually encounter strange database connection issues during performance testing. Recently, we discovered a critical issue inside the repository layer in our Spring microservice application: improper exception handling was causing unexpected failures and service disruption during performance testing. This article provides an analysis of the issue and how it was solved using annotation @Transactional
.
Spring microservice applications rely heavily on stable and efficient database interactions, often via the Java Persistence API (JPA). To maintain high performance, it is important to properly manage the connection pool and prevent connection leaks so that database interactions do not degrade application performance.
History of the problem
During a recent round of performance testing, a critical issue emerged within one of the important microservices that was used to exchange messages with the client. This service started returning repeated Gateway time-out errors. The underlying issue was in the repository layer in database operations.
Investigation revealed that the error was being thrown by a stored procedure. The error was being caused by an invalid parameter being passed to the procedure, which was causing an exception to be thrown in the business logic within the stored procedure. The repository layer could not effectively handle this exception; it was propagating it upwards. The source code for the procedure call is shown below:
public long createInboxMessage(String notifCode, String acctId, String userId, String s3KeyName,
List<Notif> notifList, String attributes, String notifTitle,
String notifSubject, String notifPreviewText, String contentType,
boolean doNotDelete, boolean isLetter, String groupId) throws EDeliveryException {
try {
StoredProcedureQuery query = entityManager.createStoredProcedureQuery("p_create_notification");
DbUtility.setParameter(query, "v_notif_code", notifCode);
DbUtility.setParameter(query, "v_user_uuid", userId);
DbUtility.setNullParameter(query, "v_user_id", Integer.class);
DbUtility.setParameter(query, "v_acct_id", acctId);
DbUtility.setParameter(query, "v_message_url", s3KeyName);
DbUtility.setParameter(query, "v_ecomm_attributes", attributes);
DbUtility.setParameter(query, "v_notif_title", notifTitle);
DbUtility.setParameter(query, "v_notif_subject", notifSubject);
DbUtility.setParameter(query, "v_notif_preview_text", notifPreviewText);
DbUtility.setParameter(query, "v_content_type", contentType);
DbUtility.setParameter(query, "v_do_not_delete", doNotDelete);
DbUtility.setParameter(query, "v_hard_copy_comm", isLetter);
DbUtility.setParameter(query, "v_group_id", groupId);
DbUtility.setOutParameter(query, "v_notif_id", BigInteger.class);
query.execute();
BigInteger notifId = (BigInteger) query.getOutputParameterValue("v_notif_id");
return notifId.longValue();
} catch (PersistenceException ex) {
logger.error("DbRepository::createInboxMessage - Error creating notification", ex);
throw new EDeliveryException(ex.getMessage(), ex);
}
}
Problem Analysis
As our scenario illustrates, when a procedure encountered an error, the exception was propagated up from the repository layer to the service layer and eventually to the controller. This propagation would cause problems that caused the API to respond with an HTTP status other than 200 — most commonly 500 or 400. After a few such procedures, the service container would reach a point where it could no longer handle incoming requests, generating a 502 Gateway Timeout error. This critical condition would be reflected in our monitoring systems, with the Kibana logs showing the problem as follows:
HikariPool-1 - Connection is not available, request timed out after 30000ms.
The problem was in incorrect exception handling, where the exception was propagated up through all layers of the system without being handled properly. This prevented database connections from being released back into the connection pool, thus depleting the available supply of connections. As a result, after the connection supply was completely exhausted, the container was unable to process new requests, which caused the error displayed in Kibana logs and in an HTTP response other than 200.
Solution
As one solution, we could gracefully handle the exception and not re-route it upstream, allowing JPA and Spring to release the connections and return them to the pool. An alternative solution is to use the annotation @Transactional
on the method. Below is the same method with annotation:
@Transactional
public long createInboxMessage(String notifCode, String acctId, String userId, String s3KeyName,
List<Notif> notifList, String attributes, String notifTitle,
String notifSubject, String notifPreviewText, String contentType, boolean doNotDelete, boolean isLetter,
String groupId) throws EDeliveryException {
………
}
The method implementation below demonstrates an approach to exception handling that prevents an exception from propagating up the stack by catching it and logging it within the method itself:
public long createInboxMessage(String notifCode, String acctId, String userId, String s3KeyName,
List<Notif> notifList, String attributes, String notifTitle,
String notifSubject, String notifPreviewText, String contentType, boolean doNotDelete, boolean isLetter,
String loanGroupId) {
try {
.......
query.execute();
BigInteger notifId = (BigInteger) query.getOutputParameterValue("v_notif_id");
return notifId.longValue();
} catch (PersistenceException ex) {
logger.error("DbRepository::createInboxMessage - Error creating notification", ex);
}
return -1;
}
Using @Transactional
annotation @Transactional
in the Spring framework manages transaction boundaries. It begins a transaction when a method marked with the annotation starts, then commits or rolls back the transaction when the method finishes. When a method throws an exception, the annotation @Transactional
provides transaction rollback, which helps to properly release the connection and return it to the pool.
Without @Transactional
When a repository method that calls a stored procedure is not marked with an annotation @Transactional
Spring does not manage transaction boundaries within this method. Transaction management must be programmed manually for the case where the stored procedure throws an exception. If this is not managed properly, it can create a situation where the database connection is not closed and returned to the connection pool, causing a leak.
The optimal approach
Always use
@Transactional
when the operations in the method must be performed within a single transaction. This is especially important for operations related to stored procedures that may modify the state of the database.Ensure that exception handling within the method is done in a way that includes proper transaction rollback and database connection closing, especially if you are not using
@Transactional
.
Conclusion
Effective transaction management is critical to maintaining the health and performance of Spring microservice applications using JPA. Abstract @Transactional
helps avoid connection leaks and ensures that database interactions do not degrade system performance or stability. Following these rules improves the reliability and efficiency of our Spring microservices, providing stable and fast response times to the consuming application or end users.
![](https://habrastorage.org/getpro/habr/upload_files/4db/32a/0a6/4db32a0a6370259c8249ec2a6f7d38ff.png)
Join the Russian-speaking Spring Boot developer community in telegram – Spring AIOto stay up to date with the latest news from the world of Spring Boot development and everything related to it.
Waiting for everybody, join us!