Pular para o conteúdo
ForceTricks
Voltar ao blog

Arquitetura de Triggers Apex: Handler Pattern e Quando Quebrá-lo

8 min de leitura
SérieApex em Orgs CorporativasParte 1 de 1
  1. 1Arquitetura de Triggers Apex: Handler Pattern e Quando Quebrá-lo

Todo projeto Salesforce eventualmente chega à mesma arquitetura de triggers: um trigger por objeto, toda a lógica delegada para uma classe handler. O padrão é sólido. Também é a primeira coisa que quebra em uma org genuinamente complexa.

Este post cobre o que faz o padrão canônico funcionar, as condições específicas em que ele para de funcionar, e as alternativas que realmente aguentam em orgs com 200+ triggers distribuídos entre múltiplos pacotes.

Por Que o Handler Pattern Funciona — e Quais Premissas Ele Assume

A abordagem padrão: um trigger por objeto dispara em todos os eventos, chama imediatamente uma classe handler, e o handler roteia a lógica por contexto.

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

public with sharing class AccountTriggerHandler extends TriggerHandler {
    public override void beforeUpdate() {
        AccountService.validarRegrasDePosse(
            (List<Account>) Trigger.new,
            (Map<Id, Account>) Trigger.oldMap
        );
    }
    public override void afterUpdate() {
        AccountService.sincronizarComERP(Trigger.new, Trigger.oldMap);
    }
}

Isso funciona de forma confiável quando três premissas são verdadeiras:

  1. Você controla todos os triggers no objeto. Um dono, um pipeline de deploy.
  2. A ordem de execução dentro da transação é irrelevante. Seus handlers não dependem de rodar antes ou depois de outro código que você não controla.
  3. O recursion guard é suficiente. Um booleano estático ou contador de profundidade basta para evitar loops infinitos.

Em uma org nova com um único time de desenvolvimento, as três premissas se sustentam. Em uma org corporativa com pacotes gerenciados, componentes ISV e múltiplas camadas de serviço, nenhuma delas é verdadeira.

Orgs Multi-Pacote: Quando Um Único Trigger É Estruturalmente Impossível

Quando um pacote gerenciado instala seu próprio trigger em Account, você passa a ter dois triggers no mesmo objeto. Você não pode mesclá-los. Não pode controlar qual dispara primeiro na maioria dos cenários. O Salesforce processa triggers na ordem em que foram criados — mas triggers de pacote precedem o seu código, então eles rodam primeiro.

O problema piora quando você adiciona uma camada de customização em cima de um pacote gerenciado. Agora você tem:

  • O trigger do pacote gerenciado (dispara primeiro, sem visibilidade)
  • Seu trigger de customização (dispara segundo, depende do que o pacote fez)
  • Potencialmente um trigger de camada de serviço de uma ferramenta de integração (dispara terceiro)

Os três estão em Account. Os três chamam seus próprios handlers. Nenhum sabe da existência dos outros.

O ideal de "um trigger por objeto" é estruturalmente impossível aqui — você já tem três triggers, e dois deles você não escreveu.

O que realmente importa nesse cenário não é eliminar múltiplos triggers. É controlar o que seus triggers fazem em relação ao que você não pode controlar. O trabalho real é isolar sua lógica dos efeitos colaterais causados pelo código do pacote que roda na mesma transação.

Padrões que Funcionam em Ambientes de Alta Complexidade

Ordered Dispatcher Pattern

Em vez de um handler por objeto, você registra handlers em um dispatcher com ordenação explícita. Cada handler declara em qual contexto roda e com qual prioridade.

public with sharing class TriggerDispatcher {
    private static Map<String, List<ITriggerHandler>> registry =
        new Map<String, List<ITriggerHandler>>();

    public static void register(String key, ITriggerHandler handler, Integer ordem) {
        if (!registry.containsKey(key)) {
            registry.put(key, new List<ITriggerHandler>());
        }
        // Insere na posição de ordem
        registry.get(key).add(handler);
        registry.get(key).sort(); // assume que ITriggerHandler implementa Comparable
    }

    public static void run(String tipoObjeto, System.TriggerOperation op) {
        String key = tipoObjeto + '.' + op.name();
        List<ITriggerHandler> handlers = registry.get(key) ?? new List<ITriggerHandler>();
        for (ITriggerHandler h : handlers) {
            h.execute(Trigger.new, Trigger.old, Trigger.newMap, Trigger.oldMap);
        }
    }
}

O benefício: quando o time adiciona um novo handler, ele declara sua posição explicitamente. Você enxerga a ordem de execução no código, sem precisar adivinhar datas de criação.

Context-Isolation Pattern

Quando você tem triggers de pacote gerenciado rodando na mesma transação e suspeita que estão modificando registros antes do seu código vê-los, você para de confiar diretamente no Trigger.new. Em vez disso, você re-consulta os registros depois que o trigger do pacote rodou, usando SOQL com FOR UPDATE para obter o estado confirmado.

public with sharing class AccountTriggerHandler extends TriggerHandler {
    public override void afterUpdate() {
        // Não confie em Trigger.new após um pacote gerenciado ter rodado.
        // Re-consulte com seleção explícita de campos para obter o estado atual.
        Set<Id> ids = Trigger.newMap.keySet();
        List<Account> registrosAtuais = [
            SELECT Id, OwnerId, AccountNumber, ExternalSyncStatus__c
            FROM Account
            WHERE Id IN :ids
            FOR UPDATE
        ];
        AccountService.processarPosAtualizacaoPacote(registrosAtuais, Trigger.oldMap);
    }
}

Isso custa um SOQL extra por contexto de trigger. Na maioria dos casos, a troca vale — você para de depurar estado fantasma que um pacote introduziu.

Usando a Ordem de Execução de Triggers Deliberadamente

A ordem de execução documentada do Salesforce dentro de uma transação é determinística: regras de validação, before triggers, validações do sistema, after triggers, regras de atribuição, workflow rules e assim por diante. A maioria dos desenvolvedores trata isso como conhecimento de fundo. Em orgs complexas, vira uma restrição de design.

Um padrão: adie trabalho para o contexto after-trigger quando precisar saber se as regras de validação passaram. Não replique lógica de validação no before-trigger — deixe o Salesforce fazer isso, e só atue no registro no after-trigger quando souber que o DML foi bem-sucedido.

Outro: use before delete para capturar dados de registros filhos antes que as regras de cascade os eliminem. Esse é o único lugar confiável para ler esses dados antes que desapareçam.

Nenhum dos dois é exótico. São a ordem de execução fazendo trabalho útil em vez de ser algo contra o qual você luta.

O Problema de Recursão que Guards Estáticos Não Resolvem

A prevenção clássica de recursão: um booleano estático que é definido como true quando o trigger dispara pela primeira vez, e toda chamada subsequente retorna imediatamente.

public with sharing class AccountTriggerHandler extends TriggerHandler {
    private static Boolean jaExecutou = false;

    public static void run() {
        if (jaExecutou) return;
        jaExecutou = true;
        // ... lógica
    }
}

Isso funciona quando a recursão é síncrona e acontece dentro de uma única transação. Falha em dois cenários comuns em orgs corporativas.

Chains assíncronos. Seu trigger dispara, enfileira um Queueable, o Queueable atualiza registros, o que dispara o trigger novamente — em uma transação separada. A variável estática é reiniciada entre transações. O guard estático não faz nada aqui.

Combinações de Future + Platform Event. Um método @future publica um Platform Event, o evento dispara um trigger, que publica outro Platform Event. Cada passo é uma transação separada. A recursão entre eles não é prevenida por nenhum estado estático.

A solução real para recursão assíncrona: um objeto customizado dedicado ou registro de Custom Metadata que rastreia o que o job assíncrono processou. Antes de agir, verifique o log. É mais trabalho que um booleano estático, mas é o único mecanismo que sobrevive a trocas de contexto assíncrono.

public with sharing class AccountSyncQueueable implements Queueable {
    private Set<Id> accountIds;

    public AccountSyncQueueable(Set<Id> ids) {
        this.accountIds = ids;
    }

    public void execute(QueueableContext ctx) {
        // Verifica o que já processamos nesta chain assíncrona
        Set<Id> jaProcessados = SyncProcessingLog__c.obterIdsProcessados(accountIds);
        Set<Id> aProcessar = new Set<Id>(accountIds);
        aProcessar.removeAll(jaProcessados);

        if (aProcessar.isEmpty()) return;

        SyncProcessingLog__c.marcarEmProcessamento(aProcessar);
        AccountService.sincronizarComERP(aProcessar);
    }
}

Quando o Handler Pattern É a Resposta Certa

O handler pattern é confiável quando:

  • Você é dono de todos os triggers no objeto (sem pacotes gerenciados com triggers próprios)
  • A org tem um único pipeline de deploy e um único time
  • O contexto de execução é predominantemente síncrono
  • O time é júnior a pleno — o padrão oferece estrutura previsível sem exigir conhecimento profundo de casos extremos da plataforma

Nesse ambiente, um framework TriggerHandler bem implementado (o de Kevin O'Hara ou qualquer equivalente) oferece separação clara, testabilidade e prevenção consistente de recursão. Não introduza complexidade de dispatcher ou isolamento de contexto quando a estrutura mais simples funciona.

O erro é carregar o handler pattern para um contexto para o qual ele não foi projetado e culpar o padrão quando ele luta. O padrão não está errado — o ambiente mudou.

Conclusão Prática

  • Se sua org tem pacotes gerenciados com triggers próprios → aceite múltiplos triggers por objeto e foque em isolar sua lógica dos efeitos colaterais do pacote
  • Se a ordem de execução de triggers importa para sua lógica → codifique-a explicitamente com um dispatcher em vez de depender da ordem de criação
  • Se você tem chains assíncronos que atualizam registros → guards estáticos de recursão não vão prevenir re-entrância; implemente um log de processamento
  • Se nada disso se aplica à sua org → o handler pattern padrão é a escolha certa; complexidade por conta própria não é melhoria

Primeiro post da série "Apex em Orgs Corporativas". Próximo: governor limits — as estratégias reais, não a versão do Trailhead.

Tem dúvidas sobre arquitetura de triggers na sua org? Me encontre no LinkedIn.

Compartilhar
Gabriel Cruz Ferreira

Gabriel Cruz Ferreira

Salesforce Architect · 15x Certified · Rota para CTA

Este post foi útil?