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ássicoBoolean jaRodoufalha 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:
- 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.
- 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.
- 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.
- 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