Todo dev Salesforce chega no momento em que o síncrono não dá mais conta: o callout precisa sair fora do trigger, a recalculada de carteira não cabe nos 100 SOQL da transação, o relatório tem que rodar às duas da manhã. Aí vem a pergunta que separa código que escala de código que estoura em produção na primeira carga grande: qual dos três modelos de assíncrono usar. A escolha errada não dá erro no sandbox com dez registros. Ela dá erro com dois milhões.
Use Queueable para trabalho assíncrono curto e encadeável, Batch para percorrer milhões de registros em blocos sem estourar limite, e Schedulable para disparar algo em horário fixo. Future é o modelo legado que Queueable substitui em código novo. Quando precisa agendar um processamento em massa, combine os dois: um Schedulable que enfileira um Batch.
O erro que custa caro: escolher async no chute
Em code review de org que já rodou algumas cargas grandes, o padrão que mais encontro não é a falta de assíncrono. É o assíncrono errado. O caso clássico: um @future que recalcula o valor de todas as oportunidades de uma conta. Funciona no sandbox, funciona com a conta que tem oito oportunidades, e quebra no dia em que importam uma conta com quinze mil. O método future roda tudo numa transação só, bate no limite de 50 mil registros de DML ou nos 100 SOQL, e a carga inteira falha sem ninguém saber por quê.
A raiz do problema é tratar "assíncrono" como uma coisa só. São três modelos, e cada um resolve um eixo diferente:
- Encadeamento e desacoplamento. Preciso rodar algo fora da transação atual, talvez em sequência, passando dados ricos. Esse é o território do Queueable.
- Volume. Preciso percorrer mais registros do que cabem nos limites de uma transação. Esse é o território do Batch.
- Tempo. Preciso disparar em um horário, não em resposta a um evento. Esse é o território do Schedulable.
Quem escolhe pelo eixo errado paga em produção. Vamos aos três, do mais simples ao que combina todos.
Future: o legado que você mantém, não escreve
O método @future foi a primeira forma de assíncrono no Apex, e ainda aparece em toda org com alguns anos. Ele serve para tirar trabalho da transação síncrona, tipicamente um callout que não pode rodar dentro de um trigger:
public class ContaService {
@future(callout=true)
public static void sincronizarComErp(Set<Id> contaIds) {
// callout para o ERP, fora da transacao do trigger
}
}
O problema do future não é o que ele faz, é o que ele não deixa fazer. Os parâmetros só podem ser tipos primitivos ou coleções de primitivos: você passa um Set<Id>, nunca uma List<Account>. Ele não devolve nada, então não há como monitorar se o job terminou ou falhou sem ir caçar na tabela AsyncApexJob. E, o mais limitante, um future não pode chamar outro future: acabou o encadeamento. Por isso a orientação da própria plataforma há anos é clara: em código novo, use Queueable. Future sobrevive por compatibilidade e por um punhado de contextos legados onde Queueable ainda não chega. Se você está escrevendo um future hoje sem um motivo específico, está escrevendo dívida técnica.
Queueable: o cavalo de batalha do async moderno
Queueable é o future feito direito. Ele resolve exatamente as três limitações: aceita tipos complexos, devolve um Id de job para monitorar, e encadeia. A interface é uma só, com um método:
public class RecalculoCarteira implements Queueable {
private List<Account> contas;
public RecalculoCarteira(List<Account> contas) {
this.contas = contas;
}
public void execute(QueueableContext ctx) {
// trabalho pesado aqui, com objetos ricos na mao
for (Account a : contas) {
// recalcula, monta, decide
}
update contas;
}
}
Você enfileira com System.enqueueJob(new RecalculoCarteira(minhasContas)), e recebe de volta o Id do job. O ganho real, porém, é o encadeamento. A partir do execute, você pode enfileirar o próximo Queueable, e é assim que se processa uma sequência de etapas sem tentar fazer tudo numa transação:
public void execute(QueueableContext ctx) {
processarLote(this.lote);
if (temMaisLotes()) {
System.enqueueJob(new RecalculoCarteira(proximoLote()));
}
}
Cada elo da cadeia é uma transação nova, com o conjunto de governor limits renovado. É a forma correta de dividir um trabalho grande em pedaços que cabem, sem a maquinaria do Batch. A pegadinha mora no teste: em contexto de teste, apenas um job encadeado executa, para o test run não entrar em loop infinito. Então o teste cobre a lógica do execute, e a cadeia em si você valida com um caso curto. Quando o Queueable também faz callout, ele precisa declarar isso implementando Database.AllowsCallouts, o mesmo cuidado de sempre com chamadas externas seguras.
Regra prática: se o trabalho cabe em uma transação, ou em uma cadeia curta de transações, e você quer dados ricos e monitoramento, é Queueable. Notificação proativa, sincronização de um punhado de registros, orquestração de etapas. Foi Queueable, aliás, que usei como motor no post sobre notificações proativas com Apex: o trabalho de montar e disparar a notificação sai da transação do usuário e roda desacoplado.
Batch Apex: quando o volume não cabe numa transação
O Batch existe para um problema que Queueable não resolve: percorrer mais registros do que cabem nos limites de uma única transação. Ele não processa tudo de uma vez. Ele quebra o conjunto em blocos, e cada bloco roda numa transação própria, com governor limits zerados. A classe tem três métodos, e cada um tem um papel:
public class RecalculoCarteiraBatch implements Database.Batchable<SObject> {
public Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator(
'SELECT Id, AnnualRevenue FROM Account WHERE Ativo__c = true'
);
}
public void execute(Database.BatchableContext bc, List<Account> escopo) {
for (Account a : escopo) {
// recalcula este bloco
}
update escopo;
}
public void finish(Database.BatchableContext bc) {
// roda uma vez, no fim de tudo: notifica, encadeia, fecha
}
}
O start define o universo, e usa um QueryLocator que suporta até 50 milhões de registros, muito além do teto de uma SOQL comum. O execute recebe um bloco por vez, por padrão de 200 registros, e roda uma vez por bloco. O finish roda uma única vez, quando tudo termina.
A conta que faz o Batch valer a pena é essa: imagine dois milhões de contas para recalcular. Você dispara com Database.executeBatch(new RecalculoCarteiraBatch(), 200). O Salesforce quebra isso em dez mil execuções independentes de 200 registros. Cada uma tem seus próprios 100 SOQL, seus próprios 150 DML, seus próprios 6 MB de heap. Um bloco que falha por um dado ruim não derruba os outros dez mil. Tente fazer isso num future ou num Queueable único e você estoura no primeiro punhado de milhares.
O tamanho do bloco é decisão sua, e importa. Bloco menor renova limites com mais frequência, útil quando cada registro consome muito processamento ou dispara automação pesada; bloco maior reduz o overhead de iniciar transação, útil quando o processamento por registro é leve. Se cada conta que você atualiza aciona trigger, Flow e rollup, um escopo de 200 pode ser demais: baixe para 50. Para guardar estado entre blocos, como um contador ou um acumulador, a classe implementa Database.Stateful, senão cada bloco esquece o anterior.
Regra prática: se o número de registros pode passar do que cabe nos limites de uma transação, é Batch. Sempre. Não existe "por ora são poucos": a carga que mata é a que ninguém previu.
Schedulable: rodar no horário, não no evento
Os dois primeiros respondem a um gatilho: alguém salvou um registro, um código enfileirou o job. O Schedulable responde ao relógio. A interface tem um método, e o que ele faz lá dentro é com você:
public class RecalculoNoturno implements Schedulable {
public void execute(SchedulableContext ctx) {
Database.executeBatch(new RecalculoCarteiraBatch(), 200);
}
}
Você agenda com uma expressão cron:
// toda madrugada as 2h
System.schedule('Recalculo noturno', '0 0 2 * * ?', new RecalculoNoturno());
Repare no que o execute do Schedulable faz: quase nada por conta própria. Ele dispara o Batch. Esse é o ponto que junta tudo.
Combinar os três: o padrão que resolve a maioria dos casos
Na prática, os modelos não competem, eles se encaixam. O padrão de assíncrono mais comum em produção não é escolher um, é agendar um processamento em massa que continua desacoplado no fim. A cadeia inteira fica assim:
- Um Schedulable acorda às 2h da manhã.
- Ele enfileira um Batch que percorre milhões de registros em blocos.
- No
finishdo Batch, um Queueable é enfileirado para o trabalho leve de fechamento: montar o resumo, disparar a notificação, chamar um sistema externo.
Na org de demonstração que mantenho para a Vetra Distribuidora (empresa fictícia do meu portfólio), é exatamente esse o desenho da recalculada de carteira. Um Schedulable dispara às 2h; o Batch reprocessa as contas ativas em blocos de 200; o finish enfileira um Queueable que faz o callout para o sistema de BI e cria uma notificação proativa para cada gerente cujo território mudou de faixa. Cada peça faz uma coisa, no eixo certo: o Schedulable cuida do tempo, o Batch cuida do volume, o Queueable cuida do encadeamento e do callout. Nenhuma delas tenta fazer o trabalho da outra, e por isso nenhuma estoura.
public void finish(Database.BatchableContext bc) {
// volume ja foi tratado; agora o fechamento leve e desacoplado
System.enqueueJob(new NotificarGerentes(bc.getJobId()));
}
Esse encaixe é o que transforma três recursos separados numa arquitetura. Assim como um trigger handler organiza o que roda em cada contexto, a escolha consciente entre os três modelos de assíncrono organiza o que roda em cada eixo: tempo, volume, encadeamento.
Tabela de decisão: qual escolher
Quando a dúvida bater, a pergunta não é "qual é o mais poderoso", é "qual eixo o meu problema tem". A tabela abaixo resume o critério:
| Modelo | Use quando | Limite prático | Aceita callout | Encadeia |
|---|---|---|---|---|
| Future | Só em código legado ou contexto que Queueable não alcança | Parâmetros só primitivos, sem monitoramento | Sim, com callout=true |
Não |
| Queueable | Trabalho curto, dados ricos, etapas encadeadas | Cabe em uma transação por elo da cadeia | Sim, com Database.AllowsCallouts |
Sim |
| Batch | Volume acima do que cabe numa transação | Até 50 milhões de registros no QueryLocator |
Sim, com Database.AllowsCallouts |
Sim, via finish |
| Schedulable | Disparo por horário, não por evento | Poucos jobs agendados simultâneos por org | Indiretamente, via o que dispara | Sim, dispara Batch ou Queueable |
A leitura da tabela é direta. Comece pelo eixo: é tempo, é volume, ou é encadeamento e desacoplamento? Se for volume, é Batch, ponto final. Se for tempo, é Schedulable disparando um dos outros. Se for encadeamento com dados ricos e couber nos limites, é Queueable. E Future só entra na conversa se você está mantendo código antigo. Escolher pelo eixo, e não pelo hábito, é o que evita a carga que estoura no dia em que os dados crescem.
Perguntas frequentes
Qual a diferença entre Future e Queueable no Salesforce?
Queueable faz tudo o que Future faz e mais. Aceita tipos complexos como objetos e coleções (não só primitivos), devolve um Id de job para monitorar, e permite encadear um job a partir de outro. Future é o modelo legado. Em código novo, use Queueable; Future só sobrevive por compatibilidade e por contextos legados que Queueable ainda não alcança.
Quantos registros o Batch Apex processa de uma vez?
Ele não processa tudo de uma vez, e é isso que o torna útil. O Batch quebra o conjunto em blocos, por padrão de 200 registros, e cada bloco roda numa transação própria com os governor limits renovados. Dois milhões de registros com escopo 200 viram dez mil execuções independentes, e o QueryLocator do start suporta até 50 milhões.
Como agendar um Batch para rodar toda madrugada?
Você não agenda o Batch direto. Cria uma classe Schedulable cujo execute chama Database.executeBatch da sua classe de Batch, e agenda essa Schedulable com uma expressão cron via System.schedule ou pela tela de jobs agendados. O Schedulable dispara no horário, o Batch faz o trabalho pesado em blocos.
Posso encadear Queueable indefinidamente?
Em produção, na prática não há teto rígido de profundidade de cadeia: cada Queueable enfileira o próximo e o trabalho continua. Em contexto de teste, porém, só um job encadeado executa, para evitar loop infinito no test run. Encadear é a forma correta de continuar o trabalho, em vez de tentar disparar tudo de uma vez.
