No Spring, Propagation não significa o que você acha

@Transactional muitas vezes não faz o que o nome sugere.

Boa parte da confusão vem da enumeração de Propagation.

Você lê REQUIRED, REQUIRES_NEW, SUPPORTS, MANDATORY, NOT_SUPPORTED, NEVER e parece que o comportamento já está óbvio.

Só que esses nomes são muito mais perigosos do que parecem quando a leitura fica superficial.


O primeiro erro: ler o nome e imaginar a semântica

Muita gente trata propagation como se fosse uma preferência declarativa.

"Aqui eu quero uma transação nova."

"Aqui tanto faz."

"Aqui é só leitura."

Mas propagation não é intenção solta.

É regra de execução.

E essa regra só entra em cena quando a chamada foi realmente interceptada.

É exatamente o ponto do post sobre @Transactional e proxy: antes de discutir o valor da propagation, você precisa ter certeza de que o método passou pela fronteira onde o Spring consegue agir.


O que os principais valores realmente fazem

Antes de entrar nos mal-entendidos mais comuns, vale traduzir a enumeração para semântica de execução:

  • REQUIRED: entra na transação atual; se não existir, abre uma
  • REQUIRES_NEW: suspende a transação atual e abre outra
  • SUPPORTS: participa da atual se existir; senão, executa sem transação
  • MANDATORY: exige que já exista uma transação ativa
  • NOT_SUPPORTED: suspende a atual e executa fora de transação
  • NEVER: falha se existir transação ativa
  • NESTED: tenta criar um escopo aninhado com savepoint, quando o ambiente suporta

O problema é que esses nomes parecem descrever intenção de negócio.

Mas eles descrevem apenas como o Spring deve reagir diante da presença ou ausência de transação ativa no momento da chamada.


REQUIRED não quer dizer "sempre transacional do mesmo jeito"

REQUIRED é o padrão.

E justamente por isso ele costuma ser lido de forma preguiçosa.

Muita gente assume que REQUIRED significa simplesmente "este método é transacional".

Só que a regra real é:

  • se já existe transação, ele participa dela;
  • se não existe, ele abre uma.

Na prática, o mesmo método pode:

  • abrir sua própria transação quando chamado de fora;
  • apenas entrar na transação de outro método quando chamado dentro de um fluxo já aberto.

Ou seja: até o valor mais comum muda de comportamento conforme o contexto.


REQUIRES_NEW não cria mágica por presença

REQUIRES_NEW significa:

se a chamada for interceptada, o Spring suspende a transação atual e abre outra.

Esse detalhe importa.

O que abre a nova transação não é o texto da anotação.

É a interceptação da chamada.

Por isso este código engana tanto:

@Transactional
public void processar(Long pedidoId) {
    registrarAuditoria(pedidoId);
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void registrarAuditoria(Long pedidoId) {
    auditoriaRepository.save(new Auditoria("PROCESSANDO", pedidoId));
}

Se registrarAuditoria() for chamado internamente na mesma classe, o proxy fica fora da conversa.

Nesse caso, o REQUIRES_NEW parece presente, mas não produz o comportamento que o time esperava.


SUPPORTS não é "tem transação, mas de boa"

SUPPORTS também é muito mal interpretado.

O nome faz muita gente imaginar algo como "participa quando dá, mas continua protegido".

Na prática, a regra é mais seca:

  • se já existir transação, o método participa dela;
  • se não existir, o método roda sem transação nenhuma.

Isso muda bastante a leitura do código.

O mesmo método pode estar transacional em um fluxo e completamente sem transação em outro.

Se o time olha apenas para a anotação e assume uma garantia uniforme, está lendo mais intenção do que semântica real.

E não: usar SUPPORTS em consulta não significa que ela virou readOnly.

Isso é outra configuração, e também costuma ser mal interpretada.


MANDATORY e NEVER são regras de contrato, não nuances

Esses dois costumam ser menos usados, mas são muito úteis para mostrar como os nomes podem enganar.

MANDATORY significa:

  • se existir transação, entra nela;
  • se não existir, lança exceção.

Ou seja: ele não "prefere" transação.

Ele exige que alguém acima já tenha aberto a fronteira correta.

NEVER faz o inverso:

  • se não existir transação, executa;
  • se existir, lança exceção.

Isso é importante em fluxos que realmente precisam garantir execução fora de contexto transacional.

O ponto é que nenhum dos dois comunica "estilo".

Eles comunicam restrição operacional.


NOT_SUPPORTED não é ausência inocente de anotação

Outro nome traiçoeiro é NOT_SUPPORTED.

Muita gente lê isso como se fosse equivalente a "não quero transação aqui".

Mas o comportamento é mais específico:

  • se houver transação ativa, o Spring suspende;
  • o método executa fora de transação;
  • depois, a transação anterior pode ser retomada.

Isso é bem diferente de simplesmente não anotar o método.

Sem anotação, você não está declarando nada.

Com NOT_SUPPORTED, você está dizendo explicitamente que, mesmo em um fluxo transacional, este trecho precisa rodar fora dele.


NESTED quase sempre é mais perigoso do que parece

NESTED costuma soar como "subtransação".

Esse nome convida muita gente a imaginar independência parecida com REQUIRES_NEW.

Mas a semântica não é essa.

Em geral, NESTED trabalha com savepoints dentro da transação existente, quando a infraestrutura suporta esse comportamento.

Isso significa que ele:

  • não é a mesma coisa que abrir uma transação completamente separada;
  • depende do suporte concreto do transaction manager e do banco;
  • pode não funcionar do jeito que o time imaginou em todos os ambientes.

Ou seja: além de depender da interceptação, ainda depende da infraestrutura.


O erro recorrente: ler nomes como intenção de negócio

É aí que a enumeração engana.

REQUIRES_NEW parece "quero isolar".

SUPPORTS parece "tanto faz".

MANDATORY parece "importante".

NEVER parece "jamais mexa nisso".

Mas o que esses valores descrevem não é o que você quer dizer para o domínio.

Eles descrevem como o framework deve se comportar na borda transacional quando a chamada chega até ele.

Se o time usa esses nomes como documentação autoexplicativa, começa a inferir garantia onde só existe regra técnica.


O que realmente importa

Antes de discutir qual propagation usar, duas perguntas vêm primeiro:

  1. Essa chamada atravessa o proxy do Spring?
  2. O time entende de forma precisa o que essa propagation faz quando há ou não há transação ativa?

Se a primeira resposta for não, o valor configurado sequer entra em ação.

Se a segunda resposta for vaga, a anotação vira uma promessa mal lida.

É por isso que propagation não deveria ser escolhida por nome bonito.

Ela deveria ser escolhida depois de responder perguntas mais concretas:

  • esse trecho deve obrigatoriamente participar da transação atual?
  • ele precisa abrir outra fronteira real?
  • ele deve falhar se não houver transação?
  • ele precisa garantir execução fora do contexto transacional?
  • o ambiente suporta o comportamento esperado, especialmente em NESTED?

O ponto que vale fixar

Propagation não é nome bonito para documentar intenção.

É comportamento de execução.

E esse comportamento só funciona quando a chamada é interceptada.

Antes de confiar no nome do enum, vale lembrar:

  • ele não explica sozinho a semântica;
  • ele não substitui desenho de fronteira;
  • e ele não faz nada se a chamada não passou pelo proxy do Spring.