Transaction Architecture & Mental Model
In a standard Spring application, the service layer defines the transaction boundary.
Spring intercepts @Transactional methods using dynamic proxies or CGLIB proxies,
delegating the actual start, commit, and rollback lifecycles to PlatformTransactionManager (such as JpaTransactionManager).
๐๏ธ Local Transaction vs Global Transaction (JTA) โ In Depth
Understanding when your application needs a simple database transaction versus a distributed multi-resource coordinator is a classic Senior / Architect interview topic.
1. Local Transaction (Single Resource)
A Local Transaction operates against one single transactional resource (typically 1 relational database connection).
- Under the hood: Delegates directly to the underlying connection driver (calls
connection.setAutoCommit(false)andconnection.commit()). - Spring Manager:
JpaTransactionManagerorDataSourceTransactionManager. - When to use: 95%+ of standard web applications where all data modifications occur in one MySQL/PostgreSQL database.
- Pros: Maximum speed, minimal latency, zero network coordination overhead.
- Limitation: Cannot guarantee atomic rollbacks if your code also writes to a second database or sends a message to RabbitMQ/Kafka.
2. Global / Distributed Transaction (JTA / XA)
A Global Transaction coordinates an atomic unit of work across two or more independent distributed resources (e.g. MySQL DB + Oracle DB + ActiveMQ / JMS queue).
- Under the hood: Managed by an external Transaction Coordinator using the XA standard and the Two-Phase Commit (2PC) protocol.
- Spring Manager:
JtaTransactionManagerdelegating to an XA coordinator (like Atomikos, Bitronix, Narayana, or J2EE app servers like WildFly/WebLogic). - When to use: Banking/financial systems where debiting Database A, crediting Database B, and publishing an audit event must all succeed or fail together.
- Cons: Heavy performance cost, locks tables across the network during 2PC, complex configuration.
Local Transaction: You walk into a hotel front desk, swipe your card, and get your room key. One vendor, one transaction, instant success or fail.
Global Transaction (2PC): A travel coordinator books a group tour involving 3 vendors: Airline (Resource 1), Hotel (Resource 2), and Charter Bus (Resource 3).
โข Phase 1 (Prepare / Vote): The coordinator calls all 3: "Can you hold 50 seats/rooms for Saturday?". All 3 lock resources and vote "YES, READY".
โข Phase 2 (Commit or Abort): Since all 3 agreed, coordinator calls everyone: "CONFIRM AND CHARGE". If the airline had said "Sorry, only 20 seats left", the coordinator immediately orders the hotel and bus to CANCEL HOLDS (Rollback).
How Two-Phase Commit (2PC) Works Internally
| Feature | Local Transaction | Global Transaction (JTA / 2PC) |
|---|---|---|
| Resource Scope | Single Database / Single Connection | Multiple Databases, JMS queues, ERPs |
| Spring Manager | JpaTransactionManager |
JtaTransactionManager |
| Protocol | Native JDBC commit() / rollback() |
XA Two-Phase Commit (2PC) |
| Latency & Throughput | Ultra high speed (< 1ms) | High latency (network round-trips & locks) |
| Modern Alternative | Standard for monolithic / modular apps | Replaced in Microservices by SAGA Pattern & Transactional Outbox |
Programmatic vs Declarative Transactions
Declarative (@Transactional)
Declare rules via annotations or XML. Spring automatically creates proxies and applies transaction advice around your methods without polluting business logic.
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public Integer addEmployee(EmployeeDTO dto) {
return employeeDAO.addEmployee(dto);
}
Programmatic (TransactionTemplate)
Explicitly control transaction boundaries in code via Spring's TransactionTemplate. Ideal when transaction boundaries must be calculated dynamically.
transactionTemplate.execute(status -> {
try {
employeeDAO.addEmployee(dto);
return null;
} catch (RuntimeException ex) {
status.setRollbackOnly();
throw ex;
}
});
Transaction Propagation Behaviors
| Propagation Mode | Behavior if Transaction Exists | Behavior if No Transaction Exists | Common Use Case |
|---|---|---|---|
REQUIRED (Default) |
Joins existing transaction | Creates a new transaction | Standard business methods |
REQUIRES_NEW |
Suspends current, starts independent tx | Creates a new transaction | Audit logging, email tracking |
MANDATORY |
Joins existing transaction | Throws Exception | Sub-operations that must never run alone |
NEVER |
Throws Exception | Executes non-transactionally | Methods forbidden from DB locking |
NOT_SUPPORTED |
Suspends current tx, runs non-tx | Executes non-transactionally | Long external REST API calls |
SUPPORTS |
Joins existing transaction | Executes non-transactionally | Read-only utility queries |
NESTED |
Creates JDBC Savepoint in current tx | Creates a new transaction | Partial rollback without failing parent |
Visual Flow of Propagation Modes
Rollback Strategy & Exception Handling
Exception, SQLException, IOException) DO NOT TRIGGER ROLLBACK unless explicitly specified with rollbackFor!
Explicit Rollback for Checked Exceptions
@Transactional(rollbackFor = { Exception.class, CustomCheckedException.class })
public void processOrder(OrderDTO order) throws Exception {
orderDAO.save(order);
if (paymentFailed()) {
throw new Exception("Payment failed"); // Will ROLLBACK!
}
}
Preventing Rollback with noRollbackFor
@Transactional(noRollbackFor = { WarningNotificationException.class })
public void registerUser(UserDTO user) {
userDAO.save(user);
// Warning exception will be thrown but user will still be COMMITTED!
throw new WarningNotificationException();
}
@Transactional method with try-catch and do not rethrow it,
Spring's proxy sees a normal return and will commit the transaction!
Spring Data JPA & Custom Repository Pattern
Spring Data JPA automatically generates implementations for repository interfaces. When you need complex native queries or customized behavior, use the Custom Repository Extension Pattern:
Key Annotations Cheat Sheet
@Query("SELECT e FROM EmployeeEntity e WHERE e.salary >= ?1")โ Custom JPQL Query with positional parameter.@Query("SELECT e FROM EmployeeEntity e WHERE e.salary >= :salary")โ Custom JPQL with named parameter matching@Param("salary").@Modifyingโ Required forUPDATEandDELETEqueries executed via@Query.
Complete Connected Project Code
A production-ready Employee Management Application demonstrating JPA, Hibernate, Transactions, and Custom Spring Data Repositories.
1. pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>employee-management</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.release>17</maven.compiler.release>
<spring.version>6.2.9</spring.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>3.5.2</version>
</dependency>
<dependency>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
<version>6.6.26.Final</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.3.232</version>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>
2. src/main/resources/META-INF/persistence.xml
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="https://jakarta.ee/xml/ns/persistence" version="3.1">
<persistence-unit name="employeePU" transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<class>com.example.employee.entity.DepartmentEntity</class>
<class>com.example.employee.entity.EmployeeEntity</class>
<properties>
<property name="jakarta.persistence.jdbc.driver" value="org.h2.Driver"/>
<property name="jakarta.persistence.jdbc.url" value="jdbc:h2:mem:employee_db;DB_CLOSE_DELAY=-1"/>
<property name="jakarta.persistence.jdbc.user" value="sa"/>
<property name="jakarta.persistence.jdbc.password" value=""/>
<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
<property name="hibernate.hbm2ddl.auto" value="create-drop"/>
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
</properties>
</persistence-unit>
</persistence>
3. src/main/resources/applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:jpa="http://www.springframework.org/schema/data/jpa"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx https://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/data/jpa https://www.springframework.org/schema/data/jpa/spring-jpa.xsd">
<context:component-scan base-package="com.example.employee"/>
<tx:annotation-driven transaction-manager="transactionManager"/>
<bean id="entityManagerFactory"
class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="persistenceUnitName" value="employeePU"/>
</bean>
<bean id="transactionManager"
class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>
<jpa:repositories
base-package="com.example.employee.repository"
entity-manager-factory-ref="entityManagerFactory"
transaction-manager-ref="transactionManager"/>
</beans>
4. src/main/java/com/example/employee/entity/EmployeeEntity.java
package com.example.employee.entity;
import jakarta.persistence.*;
import java.time.LocalDate;
@Entity
@Table(name = "employee")
public class EmployeeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer employeeId;
@Column(nullable = false)
private String name;
private String role;
private Double salary;
private LocalDate joiningDate;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "department_id")
private DepartmentEntity department;
public Integer getEmployeeId() { return employeeId; }
public void setEmployeeId(Integer v) { employeeId = v; }
public String getName() { return name; }
public void setName(String v) { name = v; }
public String getRole() { return role; }
public void setRole(String v) { role = v; }
public Double getSalary() { return salary; }
public void setSalary(Double v) { salary = v; }
public LocalDate getJoiningDate() { return joiningDate; }
public void setJoiningDate(LocalDate v) { joiningDate = v; }
public DepartmentEntity getDepartment() { return department; }
public void setDepartment(DepartmentEntity v) { department = v; }
}
5. src/main/java/com/example/employee/repository/EmployeeRepository.java
package com.example.employee.repository;
import com.example.employee.entity.EmployeeEntity;
import org.springframework.data.jpa.repository.*;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import java.util.List;
public interface EmployeeRepository extends CrudRepository<EmployeeEntity, Integer> {
// Derived Query Method
List<EmployeeEntity> findBySalaryGreaterThanEqualAndRole(Double salary, String role);
// Named Parameter Query
@Query("SELECT e FROM EmployeeEntity e WHERE e.salary >= :salary")
List<EmployeeEntity> findByMinimumSalaryNamed(@Param("salary") Double salary);
// Modifying Query
@Modifying
@Query("UPDATE EmployeeEntity e SET e.salary = :salary WHERE e.role = :role")
int updateSalaryByRole(@Param("role") String role, @Param("salary") Double salary);
}
6. src/main/java/com/example/employee/service/EmployeeServiceImpl.java
package com.example.employee.service;
import com.example.employee.dao.DepartmentDAO;
import com.example.employee.dao.EmployeeDAOWrapper;
import com.example.employee.dto.DepartmentDTO;
import com.example.employee.dto.EmployeeDTO;
import com.example.employee.repository.EmployeeRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.*;
@Service
public class EmployeeServiceImpl implements EmployeeService {
private final EmployeeDAOWrapper employeeDAO;
private final DepartmentDAO departmentDAO;
private final EmployeeRepository repository;
public EmployeeServiceImpl(EmployeeDAOWrapper employeeDAO, DepartmentDAO departmentDAO, EmployeeRepository repository) {
this.employeeDAO = employeeDAO;
this.departmentDAO = departmentDAO;
this.repository = repository;
}
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void createDepartmentWithEmployee(String departmentName, EmployeeDTO employee) {
Integer departmentId = departmentDAO.addDepartment(new DepartmentDTO(departmentName));
employee.setDepartmentId(departmentId);
employeeDAO.addEmployee(employee);
}
@Override
@Transactional(readOnly = true)
public List<EmployeeDTO> findEmployees(LocalDate start, LocalDate end) {
return employeeDAO.findByJoiningDate(start, end);
}
}