โ† Back to Hub

Spring Transactions & Custom Repositories

Comprehensive guide to Spring Transaction Management and Spring Data JPA. Master ACID boundaries, declarative @Transactional, Proxy interception, all 7 Propagation modes, Rollback rules, Persistence Context dirty checking, and Custom Repository implementations.
Spring 6.2 Spring Data JPA 3.5 Hibernate 6.6 @Transactional 7 Propagation Modes 40 Scenario Traps Tokyo Night
01 โ€” BIG PICTURE

Transaction Architecture & Mental Model

Core Mental Model: A transaction is a safety boundary around a business operation. Either all changes commit together, or the transaction rolls back according to defined rules.

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).

sequenceDiagram autonumber participant Caller as Client / Controller participant Proxy as Spring Transactional Proxy participant TM as JpaTransactionManager participant Service as EmployeeServiceImpl participant EM as EntityManager / DB Caller->>Proxy: addEmployee(dto) Proxy->>TM: 1. Get / Begin Transaction TM-->>Proxy: TransactionStatus (ACTIVE) Proxy->>Service: 2. Invoke target method Service->>EM: persist(entity) EM-->>Service: Entity Managed Service-->>Proxy: 3. Return successfully Proxy->>TM: 4. Commit Transaction TM->>EM: flush() & DB Commit Proxy-->>Caller: 5. Return Employee ID

๐Ÿ›๏ธ 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) and connection.commit()).
  • Spring Manager: JpaTransactionManager or DataSourceTransactionManager.
  • 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: JtaTransactionManager delegating 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.
๐Ÿ–๏ธ Real-World Analogy (Group Trip Booking):

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

sequenceDiagram autonumber participant App as Service Method participant Coord as JTA Coordinator (Atomikos) participant DB1 as Database 1 (MySQL XA) participant DB2 as Database 2 (Oracle XA) participant JMS as Queue (ActiveMQ XA) App->>Coord: 1. Begin Global Transaction Coord-->>App: Global Transaction Active App->>DB1: Write Order Record App->>DB2: Deduct Inventory App->>JMS: Queue Payment Event App->>Coord: 2. Commit Request Note over Coord,JMS: PHASE 1: PREPARE (Voting Phase) Coord->>DB1: XA PREPARE? DB1-->>Coord: VOTE_COMMIT (Ready) Coord->>DB2: XA PREPARE? DB2-->>Coord: VOTE_COMMIT (Ready) Coord->>JMS: XA PREPARE? JMS-->>Coord: VOTE_COMMIT (Ready) Note over Coord,JMS: PHASE 2: COMMIT (Execution Phase) Coord->>DB1: XA COMMIT Coord->>DB2: XA COMMIT Coord->>JMS: XA COMMIT Coord-->>App: Global Transaction Committed Successfully
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
๐Ÿ’ก Microservices Interview Pro-Tip: Why is JTA rarely used in cloud microservices? Because 2PC requires distributed table locks across networks, creating severe bottlenecks and single points of failure. Modern cloud systems favor Eventual Consistency using Saga Orchestration (compensating actions) or the Transactional Outbox Pattern via Kafka/Debezium.
Key Rule: Local = 1 Database (JpaTransactionManager). Global = Multi-resource 2PC (JtaTransactionManager).
02 โ€” STYLES

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;
    }
});
03 โ€” PROPAGATION

Transaction Propagation Behaviors

Propagation Question: If Method A (already transactional) calls Method B, what should happen to the transaction?
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

flowchart TD Start["Method Invoked"] --> Check{"Is Transaction Active?"} Check -->|Yes| ReqY["REQUIRED: Join current"] Check -->|No| ReqN["REQUIRED: Start new"] Check -->|Yes| NewY["REQUIRES_NEW: Suspend outer, start independent"] Check -->|No| NewN["REQUIRES_NEW: Start new"] Check -->|Yes| MandY["MANDATORY: Join current"] Check -->|No| MandN["MANDATORY: Throw IllegalTransactionStateException"] Check -->|Yes| NevY["NEVER: Throw IllegalTransactionStateException"] Check -->|No| NevN["NEVER: Run non-transactionally"]
04 โ€” ROLLBACK RULES

Rollback Strategy & Exception Handling

Default Spring Rule: Spring rolls back ONLY for Unchecked Exceptions (RuntimeException & Error) by default. Checked Exceptions (e.g. 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();
}
Interview Trap (Swallowed Exceptions): If you catch an exception inside a @Transactional method with try-catch and do not rethrow it, Spring's proxy sees a normal return and will commit the transaction!
05 โ€” REPOSITORIES

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:

classDiagram class CrudRepository { <> +save(entity) +findById(id) +findAll() +delete(entity) } class EmployeeRepositoryCustom { <> +updateSalaryByRole(role, salary) +findSeniorEmployees() } class EmployeeRepository { <> +findByJoiningDate(start, end) } class EmployeeRepositoryImpl { -EntityManager entityManager +updateSalaryByRole(role, salary) +findSeniorEmployees() } CrudRepository <|-- EmployeeRepository EmployeeRepositoryCustom <|-- EmployeeRepository EmployeeRepositoryCustom <|.. EmployeeRepositoryImpl

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 for UPDATE and DELETE queries executed via @Query.
06 โ€” CODE FILES

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);
    }
}
07 โ€” INTERVIEW TRAPS & SCENARIOS

Scenario-Based Interview Questions & Traps

Interview Reasoning Formula: Identify Transaction Boundary โ†’ Propagation Mode โ†’ Exception Outcome โ†’ Rollback Rule โ†’ Final DB State.

Scenario 01: Self-Invocation Trap (Proxy Bypass)

Question: What happens if Method A (non-transactional) calls Method B (annotated with @Transactional) in the same class?

Answer: The transaction is NOT created! Method calls within the same class bypass the Spring AOP Proxy.

Fix: Move Method B to another service bean or inject the self-proxy.

Scenario 02: Swallowed Exception in Try-Catch

Question: Method A is @Transactional. It catches a RuntimeException and logs it without rethrowing. Does it roll back?

Answer: NO rollback occurs. The proxy sees a normal method return and commits the database changes.

Scenario 03: Checked Exception Default Behavior

Question: A service method throws IOException (Checked). Will Spring roll back the transaction by default?

Answer: NO. By default, Spring only rolls back on RuntimeException and Error. You must declare @Transactional(rollbackFor = Exception.class).

Scenario 04: REQUIRES_NEW in Parent Transaction

Question: Service A (REQUIRED) calls Service B (REQUIRES_NEW). If Service A throws an exception after Service B finishes, does Service B roll back?

Answer: NO. Service B commits independently in its own transaction before returning to Service A.

Scenario 05: MANDATORY Propagation Failure

Question: A client calls a method marked with Propagation.MANDATORY without starting a transaction. What happens?

Answer: Spring throws an IllegalTransactionStateException immediately without executing the method.

Scenario 06: @Transactional on Private Methods

Question: What happens if you place @Transactional on a private method?

Answer: Spring's proxy mechanism ignores @Transactional on private methods. No transaction will be started.

Scenario 07: Modifying Queries without @Modifying

Question: What happens if you run an UPDATE or DELETE with @Query without adding @Modifying?

Answer: Spring throws InvalidDataAccessApiUsageException because it attempts to execute the query as a SELECT (ResultSet query).

Scenario 08: ReadOnly Transaction with Write Operation

Question: What happens if you attempt an INSERT/UPDATE in a method marked with @Transactional(readOnly = true)?

Answer: Hibernate sets FlushMode to MANUAL, omitting dirty check flushes, and the database driver may throw a read-only transaction exception.