404 Not Found

404 Not Found


nginx

The Service Layer and Transaction Management

Transactions are the guardians of data consistency—placing an order and deducting inventory must be completed atomically; either the entire transaction succeeds or the entire transaction is rolled back.

1. What You'll Learn


2. A True Story from a Business Developer

(1) Pain Point: Inconsistencies in Inventory Deductions Upon Order Placement

Alice discovered a serious bug in OrderFlow: after a user successfully placed an order, the product's inventory was not deducted. This was because creating an order and deducting inventory are two separate operations; when an order is successfully created but the inventory deduction fails, data inconsistencies occur. Charlie reported that there were already 5,000 oversold orders, resulting in a loss of over 10,000 USD for the company.

(2) Solutions for Declarative Transactions

Spring's @Transactional annotation makes transaction management a one-liner:

JAVA
@Transactional
public Order createOrder(CreateOrderRequest request) {
    Product product = productRepository.findById(request.productId()).orElseThrow();
    product.deductStock(request.quantity());    // Deduct stock
    Order order = new Order(product, request.quantity());
    return orderRepository.save(order);         // Create order
}

Failed to deduct inventory? The entire transaction is automatically rolled back, and the order is not created.

(3) Revenue

After Alice added @Transactional, inventory deductions upon order placement ensured atomicity, completely eliminating the issue of overselling. Charlie's complaint count dropped to zero.


3. Design Patterns in the Service Layer

(1) Layered Architecture

100%
graph TD
    A["Controller<br/>Request/Response"] --> B["Service<br/>Business Logic<br/>@Transactional"]
    B --> C["Repository<br/>Data Access"]
    C --> D["Database"]
Layer Responsibilities Notes Naming Conventions
Controller Receives requests, validates parameters, returns responses @RestController *Controller
Service Business Logic, Transaction Orchestration @Service *Service / *ServiceImpl
Repository Data Access @Repository *Repository

(1) ▶ Example: Service Interface and Implementation

JAVA
// OrderService.java (interface)
package com.orderflow.service;

import com.orderflow.model.Order;
public interface OrderService {
    Order createOrder(Long productId, Integer quantity);
    Order cancelOrder(Long orderId);
    Order getOrder(Long orderId);
}

// OrderServiceImpl.java (implementation)
package com.orderflow.service;

import com.orderflow.model.*;
import com.orderflow.repository.*;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderServiceImpl implements OrderService {

    private final OrderRepository orderRepository;
    private final ProductRepository productRepository;

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

    @Override
    @Transactional
    public Order createOrder(Long productId, Integer quantity) {
        Product product = productRepository.findById(productId)
            .orElseThrow(() -> new RuntimeException("Product not found"));
        product.deductStock(quantity);
        Order order = new Order();
        order.addItem(product, quantity);
        return orderRepository.save(order);
    }

    @Override
    @Transactional(readOnly = true)
    public Order getOrder(Long orderId) {
        return orderRepository.findById(orderId)
            .orElseThrow(() -> new RuntimeException("Order not found"));
    }
}

Output:

TEXT
// Execution Successful

4. @Transactional Propagation Behavior

(1) Seven Types of Communication Behaviors

Communication Behavior Meaning Typical Scenarios
REQUIRED (default) Transactions are joined, not created The vast majority of business methods
REQUIRES_NEW Always create a new transaction and suspend the current transaction Log entries (not affected by outer-level rollbacks)
NESTED Nested transactions (savepoint): If the outer transaction is rolled back, the inner transaction is also rolled back Sub-operations can be rolled back independently
SUPPORTS Transactions are involved; no non-transactional execution Query Methods
NOT_SUPPORTED Non-transactional execution; suspends the current transaction Operations that do not require a transaction
MANDATORY Must be called within a transaction; otherwise, an exception is thrown Methods that require a transaction
NEVER Must be a non-transactional call; otherwise, an exception will be thrown Transactional operations are not allowed
100%
sequenceDiagram
    participant C as Controller
    participant S1 as createOrder (REQUIRED)
    participant S2 as recordAudit (REQUIRES_NEW)
    participant S3 as deductStock (REQUIRED)

    C->>S1: Begin Tx1
    S1->>S3: Join Tx1
    S3-->>S1: OK
    S1->>S2: Suspend Tx1, Begin Tx2
    S2-->>S1: Commit Tx2, Resume Tx1
    alt Tx1 rollback
        S1-->>C: Rollback Tx1 (Tx2 not affected)
    end

(1) ▶ Example: REQUIRES_NEW ensures audit logs

JAVA
@Service
public class AuditService {

    private final AuditLogRepository auditLogRepository;

    public AuditService(AuditLogRepository auditLogRepository) {
        this.auditLogRepository = auditLogRepository;
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void logAudit(String action, String detail) {
        AuditLog log = new AuditLog(action, detail, Instant.now());
        auditLogRepository.save(log);
    }
}

Output:

TEXT
// Execution Successful
📌 Key Point: REQUIRES_NEW Commits audit logs in a separate transaction so that the logs are not lost even if the outer transaction is rolled back.


5. Transaction Rollback Rules

(1) Default rollback behavior

By default, Spring transactions only roll back RuntimeException and Error; they do not roll back due to checked exceptions.

(1) ▶ Example: rollbackFor configuration

JAVA
@Service
public class OrderServiceImpl implements OrderService {

    @Transactional(rollbackFor = Exception.class)
    public Order createOrder(Long productId, Integer quantity) throws Exception {
        Product product = productRepository.findById(productId)
            .orElseThrow(() -> new RuntimeException("Product not found"));
        product.deductStock(quantity);
        Order order = new Order();
        order.addItem(product, quantity);
        return orderRepository.save(order);
    }
}

Output:

TEXT
// Execution Successful
Configuration Meaning Applicable Scenarios
rollbackFor = Exception.class Rollback all exceptions Recommended default configuration
rollbackFor = BusinessException.class Custom exception rollback only Fine-grained control
noRollbackFor = ValidationException.class Do not roll back for specified exceptions Certain exceptions do not trigger a rollback

6. Read-Only Transaction Optimization

(1) ▶ Example: Read-Only Transactions

JAVA
@Service
public class OrderQueryService {

    private final OrderRepository orderRepository;

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

    @Transactional(readOnly = true)
    public Order getOrder(Long id) {
        return orderRepository.findById(id).orElseThrow();
    }

    @Transactional(readOnly = true)
    public Page<Order> listOrders(String status, Pageable pageable) {
        return orderRepository.findByStatus(status, pageable);
    }
}

Output:

TEXT
// Execution Successful
Dimension Read-Write Transactions Read-Only Transactions
Dirty Check Enable (Field-by-Field Comparison) Skip
Performance Slower 20-30% faster
Write Operation Allowed Prohibited (writing will throw an exception)
Applicable CUD Operations Query Operations
💡 Tip: All query methods should include @Transactional(readOnly = true); this is a zero-cost performance optimization.


7. Atomic Transaction for Order Placement and Inventory Deduction

(1) ▶ Example: The Complete Ordering Process

JAVA
@Service
public class OrderServiceImpl implements OrderService {

    private final OrderRepository orderRepository;
    private final ProductRepository productRepository;
    private final AuditService auditService;

    public OrderServiceImpl(OrderRepository orderRepository,
                            ProductRepository productRepository,
                            AuditService auditService) {
        this.orderRepository = orderRepository;
        this.productRepository = productRepository;
        this.auditService = auditService;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Order createOrder(Long productId, Integer quantity) {
        // 1. Find product
        Product product = productRepository.findById(productId)
            .orElseThrow(() -> new RuntimeException("Product not found: " + productId));

        // 2. Check stock
        if (product.getStock() < quantity) {
            throw new RuntimeException("Insufficient stock: available="
                + product.getStock() + ", requested=" + quantity);
        }

        // 3. Deduct stock (in same transaction)
        product.deductStock(quantity);

        // 4. Create order with order item
        Order order = new Order();
        order.addItem(product, quantity);
        Order saved = orderRepository.save(order);

        // 5. Audit log (in separate transaction via REQUIRES_NEW)
        auditService.logAudit("CREATE_ORDER",
            "Order " + saved.getId() + " created for product " + productId);

        return saved;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Order cancelOrder(Long orderId) {
        Order order = orderRepository.findById(orderId)
            .orElseThrow(() -> new RuntimeException("Order not found: " + orderId));

        if (!"PENDING".equals(order.getStatus())) {
            throw new RuntimeException("Cannot cancel order with status: " + order.getStatus());
        }

        // Restore stock for each item
        for (OrderItem item : order.getItems()) {
            item.getProduct().addStock(item.getQuantity());
        }

        order.setStatus("CANCELLED");
        auditService.logAudit("CANCEL_ORDER", "Order " + orderId + " cancelled");
        return order;
    }
}

Output:

TEXT
// Execution Successful

8. Comprehensive Example: Complete Implementation of the OrderFlow Service Layer

JAVA
// ProductService.java
package com.orderflow.service;

import com.orderflow.model.Product;
import com.orderflow.repository.ProductRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.List;

@Service
public class ProductService {

    private final ProductRepository productRepository;

    public ProductService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    @Transactional(readOnly = true)
    public Product getProduct(Long id) {
        return productRepository.findById(id)
            .orElseThrow(() -> new RuntimeException("Product not found"));
    }

    @Transactional(readOnly = true)
    public Page<Product> listProducts(Pageable pageable) {
        return productRepository.findAll(pageable);
    }

    @Transactional(readOnly = true)
    public List<Product> searchProducts(String keyword) {
        return productRepository.findByNameContaining(keyword);
    }

    @Transactional
    public Product createProduct(String name, BigDecimal price, Integer stock) {
        return productRepository.save(new Product(name, price, stock));
    }

    @Transactional
    public Product updateProduct(Long id, String name, BigDecimal price, Integer stock) {
        Product product = getProduct(id);
        product.setName(name);
        product.setPrice(price);
        product.setStock(stock);
        return product;
    }
}

// OrderService.java
package com.orderflow.service;

import com.orderflow.model.Order;
import com.orderflow.model.Product;
import com.orderflow.repository.OrderRepository;
import com.orderflow.repository.ProductRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderService {

    private final OrderRepository orderRepository;
    private final ProductRepository productRepository;

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

    @Transactional(rollbackFor = Exception.class)
    public Order createOrder(Long productId, Integer quantity) {
        Product product = productRepository.findById(productId)
            .orElseThrow(() -> new RuntimeException("Product not found"));
        if (product.getStock() < quantity) {
            throw new RuntimeException("Insufficient stock");
        }
        product.deductStock(quantity);
        Order order = new Order();
        order.addItem(product, quantity);
        return orderRepository.save(order);
    }

    @Transactional(rollbackFor = Exception.class)
    public Order cancelOrder(Long orderId) {
        Order order = orderRepository.findById(orderId)
            .orElseThrow(() -> new RuntimeException("Order not found"));
        order.getItems().forEach(item -> item.getProduct().addStock(item.getQuantity()));
        order.setStatus("CANCELLED");
        return order;
    }

    @Transactional(readOnly = true)
    public Order getOrder(Long orderId) {
        return orderRepository.findByIdWithItems(orderId)
            .orElseThrow(() -> new RuntimeException("Order not found"));
    }
}

❓ FAQ

Q Should the @Transactional annotation be applied to the interface or the implementation class?
A It is recommended to apply it to the methods of the implementation class. When applied to an interface, CGLIB-based proxies cannot intercept annotations on interfaces. Spring also officially recommends applying it to concrete classes.
Q Will the @Transactional annotation take effect when methods within the same class call each other?
A No. Spring transactions are proxy-based; when methods within the same class call each other, the proxy is not invoked, so the transaction does not take effect. Solutions: 1) Split them into different services; 2) Inject your own proxy @Lazy private OrderService self;.
Q What happens if a write operation is performed when readOnly=true?
A It depends on the database and connection pool configuration. Hibernate will throw an exception, while some database drivers will ignore it. Do not perform write operations within read-only transactions.
Q What is the difference between NESTED and REQUIRES_NEW?
A NESTED is a nested transaction (savepoint); when the outer transaction is rolled back, the inner transaction is also rolled back. REQUIRES_NEW is an independent transaction; rolling back the outer transaction does not affect the inner one. Use REQUIRES_NEW for independent commits, and use NESTED for partial rollbacks.
Q What are the common causes of transaction failures?
A 1) Reciprocal calls between methods of the same type; 2) Methods are not public; 3) Exceptions are swallowed by a catch block; 4) By default, only RuntimeExceptions are rolled back; checked exceptions are not rolled back.
Q How can I verify whether a transaction has taken effect?
A 1) Enable spring.jpa.show-sql=true to monitor SQL execution; 2) Set logging.level.org.springframework.transaction=DEBUG to view the transaction log; 3) Intentionally throw an exception to verify the rollback.

📖 Summary


📝 Exercises

  1. Basic Exercise (Difficulty ⭐): Add the @Transactional annotation to OrderFlow's ProductService and OrderService to verify the atomicity of inventory deduction during order placement—intentionally throw an exception after the inventory deduction to confirm that the order will not be created.

  2. Advanced Exercise (Difficulty: ⭐⭐): Create an AuditService that uses the REQUIRES_NEW propagation behavior to ensure that audit logs are not lost even if the transaction for creating an order is rolled back.

  3. Challenge Question (Difficulty: ⭐⭐⭐): Simulate a concurrent ordering scenario (two requests simultaneously attempting to purchase an item with only one unit left in stock), observe the overselling issue, and consider how to resolve it using optimistic locking (@Version) or pessimistic locking (SELECT FOR UPDATE).

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏