Pular para o conteúdo
ForceTricks
Voltar ao blog

Integração no Salesforce: implementando sem criar dívida técnica

5 min de leitura
SérieIntegração no Salesforce: do básico ao avançadoParte 2 de 3
  1. 1Integração no Salesforce: padrões que funcionam de verdade
  2. 2Integração no Salesforce: implementando sem criar dívida técnica
  3. 3Integração no Salesforce: monitoramento e o que aprendi quebrando coisas

No post anterior discuti os quatro padrões principais. Aqui entro no código — não receitas prontas, mas as estruturas que fazem a diferença entre uma integração que aguentou dois anos e uma que virou incidente no terceiro mês.

A estrutura que eu uso para chamadas HTTP

Toda chamada HTTP síncrona em Apex deveria ter três camadas:

// 1. Service — lógica de negócio, sem HTTP
public with sharing class EstoqueService {
    public static EstoqueResponse consultarEstoque(Id produtoId) {
        Product2 produto = [
            SELECT Id, ExternalId__c
            FROM Product2
            WHERE Id = :produtoId
            WITH USER_MODE
        ];
        return EstoqueClient.get(produto.ExternalId__c);
    }
}

// 2. Client — HTTP puro, sem lógica de negócio
public without sharing class EstoqueClient {
    public static EstoqueResponse get(String externalId) {
        HttpRequest req = new HttpRequest();
        req.setEndpoint('callout:EstoqueAPI/produtos/' + externalId);
        req.setMethod('GET');
        req.setTimeout(5000);

        HttpResponse res = new Http().send(req);
        if (res.getStatusCode() != 200) {
            throw new EstoqueException('HTTP ' + res.getStatusCode());
        }
        return (EstoqueResponse) JSON.deserialize(res.getBody(), EstoqueResponse.class);
    }
}

// 3. DTO — estrutura da resposta
public class EstoqueResponse {
    public Integer quantidade;
    public String unidade;
    public Datetime atualizadoEm;
}

Essa separação parece óbvia mas raramente acontece na prática. O benefício real: você consegue testar EstoqueService com um mock sem tocar no HTTP, e consegue trocar a implementação do EstoqueClient sem mexer na lógica de negócio.

Platform Events: o que ninguém fala sobre ordem

Platform Events não garantem ordem de entrega. Se você publica três eventos para o mesmo registro em sequência rápida, o consumidor pode recebê-los fora de ordem.

Para a maioria dos casos isso não importa. Mas se você está sincronizando estado — especialmente transições de status — isso quebra.

O problema de ordem em Platform Events não aparece em testes. Aparece em produção, num dia de alto volume, quando dois eventos chegam invertidos e o registro fica num estado impossível.

A solução que uso: incluir um Timestamp__c no payload e ignorar eventos mais antigos que o último processado.

public with sharing class PedidoEventHandler {
    public static void processar(List<Pedido_Atualizado__e> eventos) {
        Map<Id, PedidoSync__c> ultimoProcessado = carregarUltimoProcessado(eventos);

        for (Pedido_Atualizado__e evt : eventos) {
            PedidoSync__c ultimo = ultimoProcessado.get(evt.PedidoId__c);
            if (ultimo != null && evt.Timestamp__c <= ultimo.UltimoTimestamp__c) {
                continue; // evento desatualizado, ignora
            }
            processarEvento(evt);
        }
    }
}

Retry: o que o Salesforce faz e o que você precisa fazer

Platform Events têm retry automático por 72 horas se o subscriber falhar. Isso parece suficiente — mas não é para todos os cenários.

O retry automático não distingue falha transitória (API externa fora do ar por 2 minutos) de falha permanente (payload inválido que sempre vai falhar). Sem tratamento explícito, você pode ficar retentando um evento para sempre até a janela expirar.

public with sharing class PedidoEventHandler {
    private static final Integer MAX_TENTATIVAS = 3;

    public static void processar(List<Pedido_Atualizado__e> eventos) {
        List<IntegracaoLog__c> logs = new List<IntegracaoLog__c>();

        for (Pedido_Atualizado__e evt : eventos) {
            try {
                enviarParaSistemaExterno(evt);
                logs.add(logSucesso(evt));
            } catch (EstoqueException e) {
                Integer tentativas = obterTentativas(evt.PedidoId__c);
                if (tentativas >= MAX_TENTATIVAS) {
                    logs.add(logFalhaDefinitiva(evt, e.getMessage()));
                    notificarEquipe(evt, e);
                } else {
                    logs.add(logTentativa(evt, tentativas + 1));
                }
            }
        }
        if (!logs.isEmpty()) {
            insert logs;
        }
    }
}

Named Credentials e o erro que sempre acontece em go-live

Toda integração usa endpoint hardcoded em sandbox e Named Credential em produção — ou deveria. O problema que vejo com frequência: alguém testa com Named Credential configurada para sandbox e esquece de criar a equivalente para produção. O deploy vai, a Named Credential não existe, e a integração explode no primeiro uso.

Checklist antes de qualquer go-live de integração:

  • Named Credential criada em produção com URL correta
  • Custom Setting ou Custom Metadata com parâmetros de timeout revisados para produção
  • Mock configurado para testes (sem isso, @isTest + chamada HTTP = erro)
  • Permissão de callout concedida ao usuário de integração

O que ainda pode dar errado

Mesmo com a estrutura correta, há armadilhas que aparecem depois do go-live.

Limites de governor em contextos inesperados. O EstoqueClient chamado de um trigger via Future tem um limite de callout. Chamado de um Batch tem outro. Se o mesmo client for reutilizado em diferentes contextos, o comportamento de limite muda — e o erro que aparece não é óbvio.

Mocks mal configurados que escondem bugs reais. É comum criar um mock que sempre retorna sucesso para passar nos testes. O problema: o mock não testa o comportamento do client em falha, timeout ou resposta malformada. Testes que cobrem só o caminho feliz dão falsa segurança.

Mudança de Named Credential em produção sem aviso. Se as credenciais do sistema externo rotacionam (o que é boa prática de segurança), a Named Credential precisa ser atualizada manualmente. Não há alertas nativos para credencial expirada — a integração simplesmente começa a falhar com 401.

Deserialização silenciosa de campos opcionais. JSON.deserialize em Apex não lança exceção para campos ausentes — eles ficam nulos. Se um campo que você trata como obrigatório vier ausente numa versão nova da API, o código não quebra na linha do deserialize; quebra mais tarde, quando você tenta usar o valor nulo.

Conclusão prática

  • Se você está estruturando uma nova integração → separe Service, Client e DTO desde o primeiro commit; refatorar isso depois é mais trabalhoso do que parece
  • Se você usa Platform Events para sincronizar estado → implemente controle de ordem com Timestamp__c antes de ir para produção
  • Se o retry automático do Salesforce é o único mecanismo de resiliência → adicione controle de tentativas explícito para distinguir falhas transitórias de permanentes
  • Se o go-live está próximo → valide Named Credentials em produção com uma chamada de teste antes de liberar para usuários

No último post da série falo sobre monitoramento — porque descobrir que a integração está falhando pelo telefone do cliente não é uma estratégia.


Tem uma estrutura diferente que funciona melhor para você? Me conta no LinkedIn.

Compartilhar
Gabriel Cruz Ferreira

Gabriel Cruz Ferreira

Salesforce Architect · 15x Certified · Rota para CTA

Este post foi útil?