No Spring, @Transactional não funciona sempre
@Transactional não é garantia.
Muita gente aprende a anotação como se ela colocasse a transação dentro do método.
Anotou, então vai abrir transação. Se der erro, vai ter rollback. Se usar REQUIRES_NEW, o Spring resolve.
Na prática, não é assim.
No modo padrão do Spring, esse comportamento depende de interceptação.
Se a chamada não passar pelo proxy, a anotação pode estar no código e mesmo assim não produzir o efeito que você imaginou.
O proxy é a peça que faz a anotação agir
O Spring normalmente aplica @Transactional criando um proxy em volta do Bean.
Quando outro objeto chama esse Bean, a chamada passa antes por esse proxy.
É ali que o framework decide abrir transação, participar da atual, fazer commit ou rollback.
Esse ponto se conecta diretamente com os posts sobre Bean e IoC: o comportamento extra do Spring só existe porque o container entregou um objeto gerenciado e interceptável.
A consequência prática é simples:
se a chamada não atravessa esse proxy, @Transactional não entra em cena.
Existe uma exceção avançada importante aqui.
O modo padrão de transação é proxy, mas o Spring também permite mode = aspectj.
Nesse caso, o comportamento deixa de depender de proxy e passa a ser aplicado por weaving no bytecode via AspectJ.
Isso muda cenários como chamada interna na mesma classe e método não público.
O custo é outro: mais complexidade de setup e operação.
O caso clássico: REQUIRES_NEW que não abre nada novo
Esse é o tipo de código que engana time experiente:
@Service
public class PagamentoService {
private final PedidoRepository pedidoRepository;
private final AuditoriaRepository auditoriaRepository;
private final GatewayPagamento gatewayPagamento;
public PagamentoService(
PedidoRepository pedidoRepository,
AuditoriaRepository auditoriaRepository,
GatewayPagamento gatewayPagamento
) {
this.pedidoRepository = pedidoRepository;
this.auditoriaRepository = auditoriaRepository;
this.gatewayPagamento = gatewayPagamento;
}
@Transactional
public void processar(Long pedidoId) {
Pedido pedido = pedidoRepository.findById(pedidoId)
.orElseThrow();
pedido.marcarComoEmProcessamento();
registrarAuditoria(pedidoId);
gatewayPagamento.cobrar(pedido);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void registrarAuditoria(Long pedidoId) {
auditoriaRepository.save(new Auditoria("PROCESSANDO", pedidoId));
}
}
A leitura superficial parece ótima.
processar() abre a transação principal.
registrarAuditoria() parece abrir outra com REQUIRES_NEW, o que daria a impressão de que a auditoria ficaria salva mesmo se a cobrança falhasse depois.
Mas não é isso que acontece.
Como registrarAuditoria() está sendo chamado de dentro da mesma classe, a chamada não passa pelo proxy do Spring.
Ela vira uma chamada direta, como qualquer this.registrarAuditoria(...).
Resultado:
- o
REQUIRES_NEWnão é aplicado; - o método roda dentro da transação que já estava aberta;
- se
gatewayPagamento.cobrar(pedido)lançar exceção, a auditoria pode ser desfeita junto.
O time olha o código, vê a anotação, lê REQUIRES_NEW e assume que existe uma nova fronteira transacional.
Mas a anotação nunca teve chance real de agir.
Método private também cai na mesma armadilha
Outro caso clássico é anotar método private:
@Service
public class RelatorioService {
public void fecharMes() {
recalcularSaldos();
}
@Transactional
private void recalcularSaldos() {
// atualiza vários registros
}
}
Aqui a sensação de segurança é ainda mais enganosa.
O método está no Bean, a anotação compila e o código parece certo.
Mas, no modelo padrão baseado em proxy, esse método não virou um ponto interceptável só porque recebeu @Transactional.
De novo: a anotação está presente, mas o comportamento esperado não aparece.
A correção é explicitar a fronteira
Se você realmente precisa de outra transação, a chamada precisa atravessar outro Bean:
@Service
public class PagamentoService {
private final PedidoRepository pedidoRepository;
private final AuditoriaService auditoriaService;
private final GatewayPagamento gatewayPagamento;
public PagamentoService(
PedidoRepository pedidoRepository,
AuditoriaService auditoriaService,
GatewayPagamento gatewayPagamento
) {
this.pedidoRepository = pedidoRepository;
this.auditoriaService = auditoriaService;
this.gatewayPagamento = gatewayPagamento;
}
@Transactional
public void processar(Long pedidoId) {
Pedido pedido = pedidoRepository.findById(pedidoId)
.orElseThrow();
pedido.marcarComoEmProcessamento();
auditoriaService.registrar(pedidoId);
gatewayPagamento.cobrar(pedido);
}
}
@Service
public class AuditoriaService {
private final AuditoriaRepository auditoriaRepository;
public AuditoriaService(AuditoriaRepository auditoriaRepository) {
this.auditoriaRepository = auditoriaRepository;
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void registrar(Long pedidoId) {
auditoriaRepository.save(new Auditoria("PROCESSANDO", pedidoId));
}
}
Agora a chamada cruza a fronteira do proxy.
Só nesse cenário o Spring consegue aplicar em registrar o comportamento transacional esperado.
Quando a transação importa, a fronteira precisa aparecer no desenho da aplicação.
O ponto que vale fixar
@Transactional não é uma garantia.
No modo padrão, é um comportamento baseado em proxy.
Se a chamada não passa pelo proxy, a transação não existe.
