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_NEW nã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.