Introduction
Transaction management and caching are two important features that help ensure data integrity and improve application performance. This article delves into the operating mechanism, configuration and commonly used patterns in production.
1. @Transactional — Deep Dive
1.1 Mechanism of action
Spring uses AOP Proxy to manage transactions. When method exists @Transactional is called, the proxy will:
Client → Proxy → begin TX → Target Method → commit/rollback TX
Important: Call method
@Transactionalfrom within the same class (self-invocation) will not go through the proxy → transactions do not work.
@Service
public class OrderService {
// ✅ Transaction hoạt động khi gọi từ bên ngoài
@Transactional
public OrderResponse createOrder(CreateOrderRequest request) {
Order order = orderRepository.save(mapToEntity(request));
orderItemRepository.saveAll(mapToItems(request, order));
inventoryService.decreaseStock(request.getItems());
return OrderResponse.from(order);
}
// ❌ self-invocation — Transaction KHÔNG hoạt động
public void processOrders(List<CreateOrderRequest> requests) {
for (var request : requests) {
createOrder(request); // Gọi trực tiếp, không qua proxy
}
}
}
1.2 Propagation (Spread Transaction)
| Propagation | Description |
|---|---|
| REQUIRED (default) | Use existing TX or create new |
| REQUIRES_NEW | Always create new TX, suspend old TX |
| NESTED | Nested TX, rollback does not affect parent TX |
| SUPPORTS | Use TX if available, do not create new |
| NOT_SUPPORTED | Run outside TX, suspend TX if available |
| MANDATORY | Must have TX, throw exception otherwise |
| NEVER | There must be no TX, throw exception if there is |
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveAuditLog(AuditLog log) {
// Luôn tạo TX mới → audit log được lưu
// ngay cả khi TX gốc bị rollback
auditLogRepository.save(log);
}
1.3 Isolation Levels
@Transactional(isolation = Isolation.REPEATABLE_READ)
public AccountResponse transfer(Long fromId, Long toId, BigDecimal amount) {
Account from = accountRepository.findByIdForUpdate(fromId);
Account to = accountRepository.findByIdForUpdate(toId);
from.debit(amount);
to.credit(amount);
accountRepository.saveAll(List.of(from, to));
return AccountResponse.from(from);
}
1.4 Rollback Rules
// Rollback cho tất cả checked exceptions
@Transactional(rollbackFor = Exception.class)
public void riskyOperation() throws Exception { }
// Không rollback cho BusinessException
@Transactional(noRollbackFor = BusinessException.class)
public void processPayment() { }
1.5 Read-Only optimization
// Hibernate sẽ skip dirty checking → tăng performance
@Transactional(readOnly = true)
public Page<ProductResponse> searchProducts(Specification<Product> spec,
Pageable pageable) {
return productRepository.findAll(spec, pageable)
.map(ProductResponse::from);
}
2. Spring Cache Abstraction
2.1 Enable Cache
@Configuration
@EnableCaching
public class CacheConfig { }
2.2 Main Annotations
@Service
public class ProductService {
// Lưu kết quả vào cache "products" với key = id
@Cacheable(value = "products", key = "#id")
public ProductResponse getProduct(Long id) {
log.info("Fetching product from DB: {}", id);
return productRepository.findById(id)
.map(ProductResponse::from)
.orElseThrow(() -> new ResourceNotFoundException("Product", "id", id));
}
// Update cache khi data thay đổi
@CachePut(value = "products", key = "#id")
public ProductResponse updateProduct(Long id, UpdateProductRequest request) {
Product product = productRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Product", "id", id));
product.update(request);
return ProductResponse.from(productRepository.save(product));
}
// Xóa entry khỏi cache
@CacheEvict(value = "products", key = "#id")
public void deleteProduct(Long id) {
productRepository.deleteById(id);
}
// Xóa toàn bộ cache "products"
@CacheEvict(value = "products", allEntries = true)
public void clearProductCache() { }
}
2.3 Configuring Caffeine Cache (In-Memory)
// build.gradle.kts
implementation("com.github.ben-manes.caffeine:caffeine")
# application.yml
spring:
cache:
type: caffeine
caffeine:
spec: maximumSize=1000,expireAfterWrite=10m
@Configuration
@EnableCaching
public class CaffeineCacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
manager.setCaffeine(Caffeine.newBuilder()
.maximumSize(500)
.expireAfterWrite(Duration.ofMinutes(10))
.recordStats());
return manager;
}
}
2.4 Redis Cache (Distributed)
# application.yml
spring:
data:
redis:
host: localhost
port: 6379
cache:
type: redis
redis:
time-to-live: 600000 # 10 phút (ms)
cache-null-values: false
@Configuration
@EnableCaching
public class RedisCacheConfig {
@Bean
public RedisCacheManager cacheManager(
RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration
.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.disableCachingNullValues()
.serializeValuesWith(
SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.withCacheConfiguration("products",
config.entryTtl(Duration.ofMinutes(30)))
.build();
}
}
3. Commonly used patterns
3.1 Cache-Aside Pattern
Read: App → Cache (hit?) → Yes → return
No → DB → Update Cache → return
Write: App → DB → Evict Cache
3.2 Conditional Caching
// Chỉ cache khi result != null
@Cacheable(value = "users", key = "#email",
unless = "#result == null")
public UserResponse findByEmail(String email) { }
// Chỉ cache khi price > 100
@Cacheable(value = "products", key = "#id",
condition = "#id > 0",
unless = "#result.price < 100")
public ProductResponse getProduct(Long id) { }
Summary
- @Transactional manages transactions via AOP proxy — understand propagation, isolation, rollback rules
- Self-invocation does not go through a proxy → transactions do not work, need to separate services or use self-injection
- Spring Cache supports @Cacheable, @CachePut, @CacheEvict, easy to switch from Caffeine (in-memory) to Redis (distributed)
Exercises
- Implement money transfer between 2 accounts with @Transactional, ensuring atomicity. Test rollback when balance is insufficient
- Configure Caffeine cache for ProductService with TTL 10 minutes, max 500 entries. Measure response time before and after cache
- Create an audit log service using REQUIRES_NEW propagation so that the log is not lost when the main transaction rolls back