How do I manage transactions in Spring?

In Spring, transactions are usually managed with the @Transactional annotation.

A transaction makes sure that a group of database operations is either:

  • all succeed, or
  • all fail and roll back

This is important when one business operation changes multiple records or tables.


1. Enable Transaction Management

If you are using Spring Boot with Spring Data JPA, transaction management is usually configured automatically.

In most Spring Boot applications, you do not need to manually enable it.

If you are using plain Spring configuration, you may need:

import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@EnableTransactionManagement
public class TransactionConfig {
}

With Spring Boot, this is normally unnecessary.


2. Use @Transactional on Service Methods

The most common place to put transactions is the service layer, not the controller or repository.

Example:

package com.example.app.order;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderService {

    private final OrderRepository orderRepository;
    private final PaymentRepository paymentRepository;

    public OrderService(
            OrderRepository orderRepository,
            PaymentRepository paymentRepository
    ) {
        this.orderRepository = orderRepository;
        this.paymentRepository = paymentRepository;
    }

    @Transactional
    public void placeOrder(Order order, Payment payment) {
        orderRepository.save(order);
        paymentRepository.save(payment);
    }
}

If paymentRepository.save(payment) fails, Spring rolls back the earlier orderRepository.save(order) operation.


3. Use readOnly = true for Query Methods

For methods that only read data, use:

@Transactional(readOnly = true)

Example:

package com.example.app.order;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
public class OrderQueryService {

    private final OrderRepository orderRepository;

    public OrderQueryService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    @Transactional(readOnly = true)
    public List<Order> findAllOrders() {
        return orderRepository.findAll();
    }
}

readOnly = true can help performance and communicates that the method should not modify data.


4. Rollback Behavior

By default, Spring rolls back a transaction for:

  • RuntimeException
  • Error

By default, Spring does not roll back for checked exceptions.

Example:

@Transactional
public void updateOrder() {
    throw new IllegalStateException("Something failed");
}

This transaction rolls back because IllegalStateException is a runtime exception.


5. Roll Back for Checked Exceptions

If you want rollback for a checked exception, specify rollbackFor.

@Transactional(rollbackFor = Exception.class)
public void importOrders() throws Exception {
    // database changes
    throw new Exception("Import failed");
}

You can also target a specific exception:

@Transactional(rollbackFor = OrderImportException.class)
public void importOrders() throws OrderImportException {
    // database changes
    throw new OrderImportException("Import failed");
}

6. Avoid Catching Exceptions Without Rethrowing

This can prevent rollback:

@Transactional
public void placeOrder(Order order) {
    try {
        orderRepository.save(order);
        paymentService.charge(order);
    } catch (Exception ex) {
        // Bad if you swallow the exception
    }
}

If the exception is caught and not rethrown, Spring may think the method completed successfully and commit the transaction.

Prefer:

@Transactional
public void placeOrder(Order order) {
    try {
        orderRepository.save(order);
        paymentService.charge(order);
    } catch (Exception ex) {
        throw new OrderProcessingException("Could not place order", ex);
    }
}

7. Transaction Boundaries Should Match Business Operations

A transaction should usually wrap one complete business action.

Good examples:

@Transactional
public void transferMoney(Long fromAccountId, Long toAccountId, BigDecimal amount) {
    debitAccount(fromAccountId, amount);
    creditAccount(toAccountId, amount);
}
@Transactional
public void registerUser(RegisterUserRequest request) {
    createUser(request);
    createDefaultSettings(request);
    sendWelcomeEvent(request);
}

Avoid making transactions too large, especially if they include slow external calls.


8. Be Careful with External API Calls

Avoid doing slow network calls inside a database transaction when possible.

Less ideal:

@Transactional
public void placeOrder(Order order) {
    orderRepository.save(order);
    paymentGateway.charge(order); // external network call inside transaction
    order.setStatus(OrderStatus.PAID);
}

Better pattern:

@Transactional
public Order createPendingOrder(Order order) {
    order.setStatus(OrderStatus.PENDING_PAYMENT);
    return orderRepository.save(order);
}

@Transactional
public void markOrderPaid(Long orderId) {
    Order order = orderRepository.findById(orderId)
            .orElseThrow();

    order.setStatus(OrderStatus.PAID);
}

Then call the payment gateway between those operations.


9. Common @Transactional Options

@Transactional(
        readOnly = false,
        rollbackFor = Exception.class,
        timeout = 10
)
public void processOrder() {
    // database work
}

Common options:

Option Meaning
readOnly Marks the transaction as read-only
rollbackFor Exceptions that should trigger rollback
noRollbackFor Exceptions that should not trigger rollback
timeout Maximum transaction duration in seconds
propagation How this method joins or creates transactions
isolation How isolated this transaction is from other transactions

10. Propagation Basics

Propagation controls what happens if a transactional method calls another transactional method.

The default is:

Propagation.REQUIRED

That means:

  • join the existing transaction if one exists
  • otherwise create a new transaction

Example:

@Transactional
public void checkout() {
    reserveInventory();
    chargePayment();
}

If reserveInventory() and chargePayment() are also transactional with default propagation, they participate in the same transaction.

Common propagation values:

Propagation Meaning
REQUIRED Use current transaction or create one
REQUIRES_NEW Always start a new transaction
MANDATORY Must already have a transaction
SUPPORTS Use a transaction if one exists
NOT_SUPPORTED Run without a transaction
NEVER Fail if a transaction exists
NESTED Use a nested transaction if supported

Example using a separate transaction for audit logging:

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveAuditLog(String message) {
    auditLogRepository.save(new AuditLog(message));
}

11. Isolation Basics

Isolation controls how much one transaction can see changes from another transaction.

Example:

@Transactional(isolation = Isolation.READ_COMMITTED)
public void processPayment() {
    // database work
}

Common isolation levels:

Isolation Meaning
DEFAULT Use the database default
READ_UNCOMMITTED May read uncommitted changes
READ_COMMITTED Only read committed data
REPEATABLE_READ Same row read twice stays consistent
SERIALIZABLE Strongest isolation, lowest concurrency

Most applications use the database by default unless there is a specific consistency problem.


12. Important Limitation: Self-Invocation

Spring transactions are usually applied through proxies.

That means this may not start a transaction as expected:

@Service
public class UserService {

    public void outerMethod() {
        innerMethod();
    }

    @Transactional
    public void innerMethod() {
        // database work
    }
}

Because innerMethod() is called from the same class, the call may bypass Spring’s transactional proxy.

Prefer calling transactional methods from another Spring bean, or put @Transactional on the outer method:

@Service
public class UserService {

    @Transactional
    public void outerMethod() {
        innerMethod();
    }

    public void innerMethod() {
        // database work
    }
}

13. Recommended Structure

A typical Spring application uses transactions like this:

Controller
    ↓
Service  ← @Transactional here
    ↓
Repository
    ↓
Database

Example:

@RestController
public class OrderController {

    private final OrderService orderService;

    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }

    @PostMapping("/orders")
    public void createOrder(@RequestBody Order order) {
        orderService.createOrder(order);
    }
}
@Service
public class OrderService {

    private final OrderRepository orderRepository;

    public OrderService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    @Transactional
    public void createOrder(Order order) {
        orderRepository.save(order);
    }
}

Quick Rules

Use these defaults for most Spring applications:

  1. Put @Transactional on service methods.
  2. Use @Transactional(readOnly = true) for query methods.
  3. Use @Transactional for create, update, and delete methods.
  4. Do not swallow exceptions inside transactional methods.
  5. Use rollbackFor if you need rollback for checked exceptions.
  6. Keep transactions short.
  7. Avoid slow external API calls inside transactions.
  8. Be aware that self-invocation can bypass transactional behavior.

For most Spring Boot + Spring Data JPA applications, this is enough:

@Service
public class UserService {

    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Transactional(readOnly = true)
    public List<User> findUsers() {
        return userRepository.findAll();
    }

    @Transactional
    public User createUser(User user) {
        return userRepository.save(user);
    }
}

How do I connect Spring to a database?

To connect Spring to a database, the usual modern approach is:

  1. Add database-related dependencies.
  2. Configure the database connection properties.
  3. Create an entity.
  4. Create a repository.
  5. Use the repository from a service or controller.

The simplest way is with Spring Boot + Spring Data JPA.


1. Add Maven Dependencies

For a Spring Boot application using JPA, add:

<dependencies>
    <!-- Spring Data JPA -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>

    <!-- Example: PostgreSQL driver -->
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

For MySQL instead:

<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>

If you are also building REST endpoints, you usually include:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

2. Configure application.properties

Create or update:

src/main/resources/application.properties

Example for PostgreSQL:

spring.datasource.url=jdbc:postgresql://localhost:5432/app
spring.datasource.username=postgres
spring.datasource.password=postgres

spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

Example for MySQL:

spring.datasource.url=jdbc:mysql://localhost:3306/app
spring.datasource.username=root
spring.datasource.password=secret

spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

Spring Boot will automatically create a DataSource, configure Hibernate, and connect Spring Data JPA to the database.


3. Create an Entity

Example:

package com.example.app.user;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.Setter;

@Entity
@Getter
@Setter
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private String email;
}

The @Entity annotation tells JPA that this class maps to a database table.


4. Create a Repository

package com.example.app.user;

import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {
}

This gives you built-in methods such as:

findAll()
findById(id)
save(entity)
deleteById(id)

You do not need to manually open JDBC connections for common CRUD operations.


5. Use the Repository in a Service

package com.example.app.user;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
public class UserService {

    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Transactional(readOnly = true)
    public List<User> findAll() {
        return userRepository.findAll();
    }

    @Transactional
    public User save(User user) {
        return userRepository.save(user);
    }
}

Use @Transactional for methods that interact with the database.


6. Optional REST Controller Example

package com.example.app.user;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/users")
    public List<User> users() {
        return userService.findAll();
    }
}

When you visit:

http://localhost:8080/users

Spring will query the database through the repository and return the users as JSON.


7. Recommended ddl-auto Values

The property:

spring.jpa.hibernate.ddl-auto=update

controls how Hibernate manages tables.

Common values:

Value Meaning
none Do not change the schema
validate Check that tables match entities
update Update tables automatically
create Drop and recreate tables on startup
create-drop Create on startup, drop on shutdown

For learning, update is convenient.

For production, prefer:

spring.jpa.hibernate.ddl-auto=validate

and use a migration tool such as Flyway or Liquibase.


8. Typical Spring Database Flow

Controller
    ↓
Service
    ↓
Repository
    ↓
Spring Data JPA / Hibernate
    ↓
DataSource
    ↓
Database

Quick Checklist

To connect Spring to a database:

  1. Add spring-boot-starter-data-jpa.
  2. Add the database driver, such as PostgreSQL or MySQL.
  3. Configure spring.datasource.url, username, and password.
  4. Create an @Entity.
  5. Create a JpaRepository.
  6. Inject the repository into a service.
  7. Use @Transactional for database operations.

For most Spring applications, you should let Spring Boot configure the DataSource automatically instead of manually creating JDBC connections.