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
- Service-layer design patterns: Interface + Implementation,
@Serviceannotation and dependency injection @TransactionalPropagation Behavior (REQUIRED, REQUIRES_NEW, NESTED)- Transaction rollback rules:
rollbackFor/noRollbackForconfiguration - Read-Only Transactions
@Transactional(readOnly = true)Performance Optimization - Alice: Implement an atomic transaction for deducting inventory upon order placement
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:
@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
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
// 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:
// 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 |
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
@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:
// Execution Successful
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
@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:
// 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
@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:
// 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 |
@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
@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:
// Execution Successful
8. Comprehensive Example: Complete Implementation of the OrderFlow Service Layer
// 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
@Lazy private OrderService self;.readOnly=true?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
- The Service layer encapsulates business logic; the Controller calls the Service, and the Service calls the Repository.
@TransactionalDefault REQUIRED propagation; RuntimeException is automatically rolled backREQUIRES_NEWis used for standalone transactions (such as audit logs), andNESTEDis used for nested transactionsrollbackFor = Exception.classEnsure that all exceptions result in a rollback@Transactional(readOnly = true)Zero-cost optimization for query methods- Transactions involving mutual switching between methods of the same type do not take effect; this is a limitation of Spring's proxy mechanism.
📝 Exercises
-
Basic Exercise (Difficulty ⭐): Add the
@Transactionalannotation 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. -
Advanced Exercise (Difficulty: ⭐⭐): Create an AuditService that uses the
REQUIRES_NEWpropagation behavior to ensure that audit logs are not lost even if the transaction for creating an order is rolled back. -
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).



