A Camada de Serviço e Gerenciamento de Transações
Transações são as guardiãs da consistência de dados—fazer um pedido e deduzir estoque devem ser completados atomicamente; ou toda a transação é bem-sucedida ou toda a transação é revertida.
1. O Que Você Vai Aprender
- Padrões de design da camada de serviço: Interface + Implementação, anotação
@Servicee injeção de dependência - Comportamento de Propagação do
@Transactional(REQUIRED, REQUIRES_NEW, NESTED) - Regras de rollback de transação: configuração
rollbackFor/noRollbackFor - Otimização de Desempenho com Transações Somente Leitura
@Transactional(readOnly = true) - Alice: Implementar uma transação atômica para deduzir estoque ao fazer pedido
2. Uma História Real de um Desenvolvedor de Negócio
(1) Ponto de Dor: Inconsistências nas Deduções de Estoque Ao Fazer Pedido
Alice descobriu um bug sério no OrderFlow: após um usuário fazer um pedido com sucesso, o estoque do produto não era deduzido. Isso ocorria porque criar um pedido e deduzir estoque são duas operações separadas; quando um pedido é criado com sucesso mas a dedução de estoque falha, inconsistências de dados ocorrem. Charlie relatou que já havia 5.000 pedidos com overselling, resultando em uma perda de mais de 10.000 USD para a empresa.
(2) Soluções para Transações Declarativas
A anotação @Transactional do Spring torna o gerenciamento de transações algo de uma linha:
@Transactional
public Order createOrder(CreateOrderRequest request) {
Product product = productRepository.findById(request.productId()).orElseThrow();
product.deductStock(request.quantity()); // Deduzir estoque
Order order = new Order(product, request.quantity());
return orderRepository.save(order); // Criar pedido
}
Falha ao deduzir estoque? A transação inteira é revertida automaticamente, e o pedido não é criado.
(3) Resultado
Depois que Alice adicionou @Transactional, as deduções de estoque ao fazer pedido garantiram atomicidade, eliminando completamente o problema de overselling. As reclamações de Charlie caíram para zero.
3. Padrões de Design na Camada de Serviço
(1) Arquitetura em Camadas
graph TD
A["Controller<br/>Request/Response"] --> B["Service<br/>Lógica de Negócio<br/>@Transactional"]
B --> C["Repository<br/>Acesso a Dados"]
C --> D["Database"]
| Camada | Responsabilidades | Observações | Convenção de Nomenclatura |
|---|---|---|---|
| Controller | Recebe requisições, valida parâmetros, retorna respostas | @RestController |
*Controller |
| Service | Lógica de Negócio, Orquestração de Transações | @Service |
*Service / *ServiceImpl |
| Repository | Acesso a Dados | @Repository |
*Repository |
(1) ▶ Exemplo: Interface e Implementação de Service
// 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 (implementação)
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"));
}
}
Saída:
// Execução bem-sucedida
4. Comportamento de Propagação do @Transactional
(1) Sete Tipos de Comportamentos de Comunicação
| Comportamento de Comunicação | Significado | Cenários Típicos |
|---|---|---|
REQUIRED (padrão) |
As transações são integradas, não criadas | A grande maioria dos métodos de negócio |
REQUIRES_NEW |
Sempre criar uma nova transação e suspender a transação atual | Entradas de log (não afetadas por rollbacks de nível externo) |
NESTED |
Transações aninhadas (savepoint): Se a transação externa for revertida, a interna também é | Sub-operações podem ser revertidas independentemente |
SUPPORTS |
As transações são envolvidas; execução não transacional sem transação | Métodos de Consulta |
NOT_SUPPORTED |
Execução não transacional; suspende a transação atual | Operações que não requerem transação |
MANDATORY |
Deve ser chamado dentro de uma transação; caso contrário, uma exceção é lançada | Métodos que requerem transação |
NEVER |
Deve ser uma chamada não transacional; caso contrário, uma exceção será lançada | Operações transacionais não são permitidas |
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) ▶ Exemplo: REQUIRES_NEW garante logs de auditoria
@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);
}
}
Saída:
// Execução bem-sucedida
REQUIRES_NEW Comita logs de auditoria em uma transação separada para que os logs não sejam perdidos mesmo que a transação externa seja revertida.
5. Regras de Rollback de Transação
(1) Comportamento de rollback padrão
Por padrão, transações Spring só revertem RuntimeException e Error; elas não revertem devido a exceções verificadas.
(1) ▶ Exemplo: configuração rollbackFor
@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);
}
}
Saída:
// Execução bem-sucedida
| Configuração | Significado | Cenários Aplicáveis |
|---|---|---|
rollbackFor = Exception.class |
Reverter todas as exceções | Configuração padrão recomendada |
rollbackFor = BusinessException.class |
Reverter apenas exceção personalizada | Controle refinado |
noRollbackFor = ValidationException.class |
Não reverter para exceções especificadas | Certas exceções não acionam rollback |
6. Otimização de Transações Somente Leitura
(1) ▶ Exemplo: Transações Somente Leitura
@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);
}
}
Saída:
// Execução bem-sucedida
| Dimensão | Transações de Leitura-Escrita | Transações Somente Leitura |
|---|---|---|
| Verificação Suja | Habilitada (Comparação Campo a Campo) | Ignorada |
| Desempenho | Mais lento | 20-30% mais rápido |
| Operação de Escrita | Permitida | Proibida (escrita lançará exceção) |
| Aplicável | Operações CUD | Operações de Consulta |
@Transactional(readOnly = true); esta é uma otimização de desempenho sem custo.
7. Transação Atômica para Fazer Pedido e Deduzir Estoque
(1) ▶ Exemplo: O Processo Completo de Pedidos
@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. Encontrar produto
Product product = productRepository.findById(productId)
.orElseThrow(() -> new RuntimeException("Product not found: " + productId));
// 2. Verificar estoque
if (product.getStock() < quantity) {
throw new RuntimeException("Insufficient stock: available="
+ product.getStock() + ", requested=" + quantity);
}
// 3. Deduzir estoque (na mesma transação)
product.deductStock(quantity);
// 4. Criar pedido com item de pedido
Order order = new Order();
order.addItem(product, quantity);
Order saved = orderRepository.save(order);
// 5. Log de auditoria (em transação separada 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());
}
// Restaurar estoque para cada item
for (OrderItem item : order.getItems()) {
item.getProduct().addStock(item.getQuantity());
}
order.setStatus("CANCELLED");
auditService.logAudit("CANCEL_ORDER", "Order " + orderId + " cancelled");
return order;
}
}
Saída:
// Execução bem-sucedida
8. Exemplo Abrangente: Implementação Completa da Camada de Serviço do OrderFlow
// 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"));
}
}
❓ Perguntas Frequentes
@Lazy private OrderService self;.readOnly=true?spring.jpa.show-sql=true para monitorar a execução SQL; 2) Defina logging.level.org.springframework.transaction=DEBUG para visualizar o log de transações; 3) Lance intencionalmente uma exceção para verificar o rollback.📖 Resumo
- A camada Service encapsula lógica de negócio; o Controller chama o Service, e o Service chama o Repository
@TransactionalPropagação REQUIRED padrão; RuntimeException é revertida automaticamenteREQUIRES_NEWé usado para transações independentes (como logs de auditoria), eNESTEDé usado para transações aninhadasrollbackFor = Exception.classGarante que todas as exceções resultem em rollback@Transactional(readOnly = true)Otimização sem custo para métodos de consulta- Transações envolvendo alternância mútua entre métodos do mesmo tipo não entram em vigor; esta é uma limitação do mecanismo de proxy do Spring
📝 Exercícios
-
Exercício Básico (Dificuldade ⭐): Adicione a anotação
@Transactionalao ProductService e OrderService do OrderFlow para verificar a atomicidade da dedução de estoque durante o pedido—lance intencionalmente uma exceção após a dedução de estoque para confirmar que o pedido não será criado. -
Exercício Avançado (Dificuldade: ⭐⭐): Crie um AuditService que use o comportamento de propagação
REQUIRES_NEWpara garantir que logs de auditoria não sejam perdidos mesmo que a transação de criação de pedido seja revertida. -
Questão Desafio (Dificuldade: ⭐⭐⭐): Simule um cenário de pedidos concorrentes (duas requisições simultaneamente tentando comprar um item com apenas uma unidade em estoque), observe o problema de overselling, e considere como resolvê-lo usando locking otimista (
@Version) ou locking pessimista (SELECT FOR UPDATE).



