Desenvolvimento

Trigger handler: o framework simples que impede seus triggers de virar espaguete

Ilustração em linha de fluxo de trigger organizado passando por uma classe central

Toda org Salesforce com alguns anos de estrada tem uma história parecida: um trigger nasceu pequeno, outro foi criado do lado para uma urgência, um terceiro veio junto com um pacote, e hoje ninguém sabe dizer em que ordem eles rodam nem por que uma automação dispara duas vezes. O sintoma aparece longe da causa: dado duplicado, limite estourado, teste que passa sozinho e falha em conjunto. A saída não é um framework gigante, é disciplina em uma classe.

Um trigger handler concentra a lógica de trigger de um objeto numa única classe: o trigger apenas delega, a classe separa o código por contexto (before insert, after update e os demais) e um guarda estático controla a recursão. Com isso a ordem de execução deixa de ser imprevisível e cada regra ganha um lugar único e testável.

O problema real: três triggers e ordem nenhuma

O Salesforce aceita quantos triggers você quiser no mesmo objeto. O que ele não faz é prometer a ordem entre eles. Quando AccountTrigger, AccountValidacao e AccountIntegracao disparam no mesmo update, a sequência pode variar, e qualquer regra que dependa de outra vira loteria. Em code review, esse é o padrão que mais encontro em org herdada: cada urgência dos últimos três anos virou um trigger novo, porque criar mais um era mais rápido do que entender os existentes.

O custo aparece espalhado e longe da causa:

  • Dado inconsistente que depende do dia. Duas regras escrevem no mesmo campo em triggers separados; quem vence é a ordem do sorteio.
  • Recursão descontrolada. Um trigger atualiza o registro, o que dispara os outros, que atualizam de novo. Às vezes termina em dado duplicado, às vezes em limite estourado no meio de uma carga.
  • Teste que mente. Cada trigger passa no seu teste isolado; a combinação falha em produção, onde os três rodam juntos.

A resposta da comunidade para isso tem nome e mais de uma década de estrada: um trigger por objeto, lógica no handler. Não precisa de biblioteca; precisa de uma classe e de três decisões.

A regra de ouro: o trigger tem uma linha

O trigger vira um ponto de entrada único, com todos os eventos declarados e nenhuma lógica:

trigger AccountTrigger on Account (
    before insert, before update,
    after insert, after update
) {
    AccountTriggerHandler.executar();
}

A partir daqui, a ordem das regras é decisão sua, escrita em código, e não um comportamento da plataforma. Se amanhã alguém precisar de uma regra nova, ela entra na classe, na posição certa, e não num quarto trigger concorrente.

O framework inteiro em uma classe

O despacho por contexto usa as variáveis que o próprio Trigger fornece:

public with sharing class AccountTriggerHandler {

    // Ids ja processados NESTA transacao (guarda de recursao por registro)
    private static Set<Id> jaProcessados = new Set<Id>();

    public static void executar() {
        if (Trigger.isBefore && Trigger.isInsert) {
            aplicarPadroes(Trigger.new);
        } else if (Trigger.isBefore && Trigger.isUpdate) {
            validarMudancas(Trigger.new, Trigger.oldMap);
        } else if (Trigger.isAfter && Trigger.isUpdate) {
            reagirAMudancas(Trigger.new, Trigger.oldMap);
        }
    }

    private static void aplicarPadroes(List<Account> novas) {
        for (Account a : novas) {
            if (a.Rating == null) {
                a.Rating = 'Warm';
            }
        }
    }

    private static void validarMudancas(List<Account> novas, Map<Id, Account> antigas) {
        for (Account a : novas) {
            Account antiga = antigas.get(a.Id);
            if (antiga.AnnualRevenue != null && a.AnnualRevenue == null) {
                a.addError('Faturamento nao pode ser apagado depois de informado.');
            }
        }
    }

    private static void reagirAMudancas(List<Account> novas, Map<Id, Account> antigas) {
        List<Task> tarefas = new List<Task>();
        for (Account a : novas) {
            Boolean virouCliente = a.Type == 'Customer - Direct'
                && antigas.get(a.Id).Type != 'Customer - Direct'
                && !jaProcessados.contains(a.Id);
            if (virouCliente) {
                jaProcessados.add(a.Id);
                tarefas.add(new Task(
                    WhatId = a.Id,
                    OwnerId = a.OwnerId,
                    Subject = 'Boas-vindas ao novo cliente'
                ));
            }
        }
        if (!tarefas.isEmpty()) {
            insert tarefas;
        }
    }
}

Três decisões estão embutidas aí, e cada uma paga o próprio aluguel:

  • Despacho explícito por contexto. O método executar é um índice: qualquer pessoa lê em dez segundos o que acontece em cada momento do ciclo. Regra de before muda o registro sem DML; regra de after cria efeitos colaterais. Misturar os dois é a receita clássica de recursão.
  • Métodos privados por responsabilidade. Cada regra tem nome, entrada e saída claras. Quando a regra de validação crescer, ela vira uma classe própria chamada daqui, e o índice continua legível.
  • Guarda de recursão por registro. O Set<Id> estático segura a reexecução dentro da mesma transação sem engolir registros de blocos seguintes num lote grande. O clássico Boolean jaRodou falha exatamente aí: com 201 registros, o segundo bloco é ignorado em silêncio. Esse caso é tão traiçoeiro que virou o desafio prático desta semana, com teste que prova a diferença.

Por que não um if gigante no trigger?

Porque trigger não se testa direto, classe sim. Com a lógica no handler, o teste chama AccountTriggerHandler.reagirAMudancas(novas, antigas) com listas montadas na mão e cobre o caso de borda sem depender de DML para cada caminho. É a diferença entre testar a regra e testar o encanamento. Testes que dependem de insert e update para cada cenário ficam lentos, frágeis e param de cobrir borda, que é onde os bugs moram. Se você já sofreu com cobertura alta que não pega bug real, o desafio do teste que realmente testa mostra esse buraco na prática.

Um detalhe de arquitetura que evita dor de cabeça: o handler decide o que fazer; quando fazer continua sendo decisão do ciclo do trigger. Resista à tentação de fazer o handler consultar o banco para redescobrir o que mudou: Trigger.oldMap já entrega isso de graça, sem custar uma consulta do limite de 100 SOQLs da transação.

E o Flow no meio disso?

A pergunta certa numa org moderna não é "trigger ou Flow", é "quem é o dono de cada regra". O padrão que recomendo, e que detalho em Flow ou Apex: quando usar cada um, é decidir por complexidade e volume: regra simples de campo e notificação vive bem em Flow; regra com lote grande, lógica de conjunto ou integração vive melhor no handler. O que não pode é os dois escreverem no mesmo campo sem coordenação, porque aí a reexecução cruzada volta, agora com um ator que não aparece no seu código.

Na org de demonstração que mantenho para a Vetra Distribuidora (empresa fictícia do meu portfólio), a regra é essa: automação declarativa nos processos de aprovação e notificação, handler nos cálculos de carteira e nos efeitos em lote. Desde que cada campo tem um dono único, o problema de ordem simplesmente não existe mais.

Quando um framework pronto vale a pena

Honestidade de arquiteto: para a maioria das orgs pequenas e médias, uma classe handler por objeto é o suficiente, e é tudo que o padrão acima exige. Frameworks como fflib ou Trigger Actions resolvem problemas de escala organizacional: dezenas de regras por objeto, vários times fazendo deploy no mesmo repositório, necessidade de ligar e desligar regra por configuração. Se esse é o seu contexto, eles pagam o custo. Se não é, você estaria adicionando curva de aprendizado e acoplamento para resolver um problema que ainda não tem. Dá para começar simples e migrar depois: o handler com despacho por contexto é exatamente o formato que esses frameworks esperam encontrar.

Como migrar uma org que já tem cinco triggers no objeto

Ninguém tem o luxo de parar a operação para arrumar trigger. A migração que funciona é incremental, e cabe em quatro passos:

  1. Inventarie antes de mexer. Liste os triggers do objeto, os eventos de cada um e que campos cada regra lê e escreve. Esse mapa de meia hora costuma revelar na hora onde duas regras disputam o mesmo campo, que é a origem da maioria dos bugs de ordem.
  2. Congele a criação de trigger novo. A partir de agora, regra nova entra no handler. Só essa decisão já estanca o crescimento do problema.
  3. Mova uma regra por vez, com teste antes e depois. Escreva o teste da regra no estado atual, mova a lógica para o método do handler, rode o teste de novo. Verde nos dois momentos, próxima regra. Nunca mova duas regras no mesmo deploy: quando algo quebrar, você quer saber exatamente qual movimentação foi.
  4. Apague o trigger esvaziado no mesmo deploy da última regra. Trigger vazio esquecido vira armadilha: alguém acha que ele está desativado e recria a lógica em outro lugar.

Numa org média, isso é um esforço de dias, não de meses, e cada passo deixa a org melhor do que estava, sem big bang e sem janela de risco.

Perguntas frequentes

O que é um trigger handler no Salesforce?

É uma classe Apex que concentra toda a lógica de trigger de um objeto. O trigger fica com uma linha, delegando, e a classe separa o código por contexto, controla recursão e pode ser testada diretamente.

Preciso instalar um framework pronto, como fflib ou Trigger Actions?

Para a maioria das orgs, não. Uma classe por objeto com despacho por contexto e guarda de recursão resolve sem dependência nova. Frameworks prontos fazem sentido com muitos times e objetos carregados de regras concorrentes.

Como controlar a recursão de trigger?

Com um Set<Id> estático de registros já processados na transação. O Boolean estático clássico bloqueia os blocos seguintes de um lote com mais de 200 registros e perde dados em silêncio.

É obrigatório ter um trigger só por objeto?

Obrigatório não é; a plataforma aceita vários. Mas ela não garante a ordem entre eles, então um único ponto de entrada é o que transforma a ordem das regras em decisão sua.

Seus triggers já viraram espaguete?

Eu reviso a arquitetura de triggers da sua org, mostro onde a recursão e a ordem imprevisível estão custando dados e limites, e entrego o plano de arrumação sem parar a operação. Comece com um diagnóstico gratuito de 45 minutos.

Falar no WhatsApp Ver serviços