CodeGym /Courses /Module 5. Spring /Practice: Implementing Transactions in a Spring Applicati...

Practice: Implementing Transactions in a Spring Application

Module 5. Spring
Level 6 , Lesson 5
Available

Transactions are the key mechanism for safely working with databases. Let's break down, using an online store example, why they're so important.

Imagine: a customer places an order. Sounds simple, but in reality we need to:

  1. Debit money from the customer's account
  2. Reserve the product in the warehouse
  3. Create an order record in the database

What can go wrong? Pretty much anything! For example, the money might be debited, but the server "crashes" before the order is created. Without transactions this becomes a real support nightmare. Fortunately, transactions solve this: either all operations succeed, or the system automatically rolls everything back.


Setting up TransactionManager

Before writing code, let's make sure our app is ready to handle transactions. TransactionManager is the heart of the transactional mechanism in Spring. It coordinates the execution of operations.

If you're using JPA/Hibernate (which we will), the TransactionManager setup looks simple.

Adding dependencies

The first thing we'll do is include dependencies for JPA and the database (for example, H2 Database for tests). Make sure your pom.xml contains the following:


<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
  </dependency>
  <dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
  </dependency>
</dependencies>

Database configuration

In application.properties we'll put the settings to connect to H2:


spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true

TransactionManager configuration

Spring Boot will configure TransactionManager automatically if you use spring-boot-starter-data-jpa. That means we're already ready to work with transactions. Hooray!


Implementing a transaction: creating a sample application

Now let's build a small app that models placing an order in an online store. Our tasks:

  1. Save order info.
  2. Update product available quantity.
  3. If something goes wrong, roll back all changes.

Creating entities

Start with the database and entities. We'll have two tables: Order and Product.

Product entity


import jakarta.persistence.Entity;
import jakarta.persistence.Id;

@Entity
public class Product {

    @Id
    private Long id;
    private String name;
    private int stock; // Stock count

    // Getters and setters (you can use Lombok)
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getStock() {
        return stock;
    }

    public void setStock(int stock) {
        this.stock = stock;
    }
}

Order entity


import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
public class Order {

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

    // Getters and setters
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Long getProductId() {
        return productId;
    }

    public void setProductId(Long productId) {
        this.productId = productId;
    }

    public int getQuantity() {
        return quantity;
    }

    public void setQuantity(int quantity) {
        this.quantity = quantity;
    }
}

Creating repositories

Now create repositories for our entities. We'll use JpaRepository.

ProductRepository


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

public interface ProductRepository extends JpaRepository<Product, Long> {
    // No additional methods needed for now
}

OrderRepository


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

public interface OrderRepository extends JpaRepository<Order, Long> {
    // No additional methods needed for now
}

Managing transactions via @Transactional

Now the fun part! Let's create a service that:

  1. Registers an order.
  2. Rolls back changes if there's not enough product in stock.

OrderService


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

@Service
public class OrderService {

    private final ProductRepository productRepository;
    private final OrderRepository orderRepository;

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

    @Transactional
    public void placeOrder(Long productId, int quantity) {
        // 1. Fetch the product
        Product product = productRepository.findById(productId)
                .orElseThrow(() -> new RuntimeException("Product not found!"));

        // 2. Check stock availability
        if (product.getStock() < quantity) {
            throw new RuntimeException("Not enough stock for product!");
        }

        // 3. Decrease stock
        product.setStock(product.getStock() - quantity);
        productRepository.save(product);

        // 4. Create and save the order
        Order order = new Order();
        order.setProductId(productId);
        order.setQuantity(quantity);
        orderRepository.save(order);

        // Commit changes only if everything went well
        System.out.println("Order placed successfully!");
    }
}

What does the @Transactional annotation do here? It guarantees that all changes are either committed or rolled back if an error occurs. For example, if there's not enough product in stock, the method throws an exception, and changes are rolled back.


Testing transactions

Let's test how this works! We'll write a simple test class.


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.junit.jupiter.api.Test;

@SpringBootTest
public class OrderServiceTest {

    @Autowired
    private OrderService orderService;

    @Autowired
    private ProductRepository productRepository;

    @Test
    public void testPlaceOrder_Success() {
        // Create a test product
        Product product = new Product();
        product.setId(1L);
        product.setName("Test Product");
        product.setStock(10);
        productRepository.save(product);

        // Place the order
        orderService.placeOrder(1L, 2);

        // Check product stock
        Product updatedProduct = productRepository.findById(1L).get();
        assert updatedProduct.getStock() == 8;
    }

    @Test
    public void testPlaceOrder_NotEnoughStock() {
        // Create a test product
        Product product = new Product();
        product.setId(2L);
        product.setName("Another Product");
        product.setStock(5);
        productRepository.save(product);

        try {
            // Try to order more than available in stock
            orderService.placeOrder(2L, 10);
        } catch (Exception e) {
            System.out.println(e.getMessage()); // Expect "Not enough stock for product!"
        }

        // Verify that the product didn't change
        Product updatedProduct = productRepository.findById(2L).get();
        assert updatedProduct.getStock() == 5;
    }
}

Summary

Now we have a transactional service that:

  • Ensures data integrity.
  • Automatically rolls back changes on errors.
  • Is easy to test.

In practice, these approaches make applications reliable and predictable. And yeah, don't forget to give yourself a star for today's progress!

Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION