Guide · Salesforce · Apex

Apex asynchrone : Future, Queueable, Batch, Schedulable ou événement ?

Cinq façons de sortir du traitement synchrone en Apex, quand choisir chacune, leurs limites et un exemple de code pour chaque cas, du simple callout au traitement de millions de lignes.

Pourquoi sortir du synchrone

Une transaction Apex synchrone (un enregistrement depuis l’interface, un appel d’API) doit rester courte : l’utilisateur attend, les limites sont serrées et un trigger ne peut pas faire d’appel HTTP. Dès qu’un traitement est long, volumineux ou dépend d’un service externe, on le confie à un traitement asynchrone, qui s’exécute plus tard, dans sa propre transaction, avec des limites plus larges : 200 requêtes SOQL au lieu de 100, 12 Mo de tas au lieu de 6, 60 secondes de CPU au lieu de 10.

Fil rouge : l’org fictive d’Acme Assurances, ses contrats (Contrat__c) et ses agences (Agence__c).

Choisir en un coup d’œil

BesoinOutil
Un appel HTTP après un enregistrement, sans suivi@future(callout=true)
Un traitement avec paramètres riches, suivi, chaînage ou repriseQueueable
Des milliers à des millions d’enregistrementsBatch Apex
Une exécution à heure fixeSchedulable (qui lance souvent un batch)
Réagir à un fait métier, sans couplage entre producteur et consommateursPlatform Events

@future : le plus simple

public with sharing class AgenceGeocodage {
    // Paramètres primitifs uniquement : on passe des Id, pas des enregistrements.
    @future(callout=true)
    public static void geocoder(Set<Id> agenceIds) {
        List<Agence__c> agences = [SELECT Id, Adresse__c FROM Agence__c WHERE Id IN :agenceIds WITH USER_MODE];
        for (Agence__c a : agences) {
            a.Coordonnees__Latitude__s = ServiceGeo.latitude(a.Adresse__c);
        }
        update as user agences;
    }
}

Simple, mais limité : paramètres primitifs uniquement (des Id, pas des objets), aucun identifiant de job pour suivre l’exécution, impossible d’appeler une autre méthode @future depuis celle-ci. Pour tout nouveau code, Queueable fait la même chose en mieux.

Queueable : le choix par défaut

Un Queueable accepte des paramètres de tout type, renvoie un identifiant de job, peut appeler des services externes (Database.AllowsCallouts) et se chaîner : chaque exécution traite un lot et relance la suite.

public with sharing class SyncContratsJob implements Queueable, Database.AllowsCallouts {
    private final List<Id> contratIds;

    public SyncContratsJob(List<Id> contratIds) {
        this.contratIds = contratIds;
    }

    public void execute(QueueableContext context) {
        // Le Finalizer s'exécute même si ce job échoue sur une limite non interceptable.
        System.attachFinalizer(new SyncContratsFinalizer(contratIds));

        List<Id> lot = new List<Id>();
        List<Id> reste = new List<Id>();
        for (Integer i = 0; i < contratIds.size(); i++) {
            if (i < 50) {
                lot.add(contratIds[i]);
            } else {
                reste.add(contratIds[i]);
            }
        }
        ServiceExterne.envoyer([SELECT Id, Numero__c, Statut__c FROM Contrat__c WHERE Id IN :lot]);

        // Chaînage : un job enfant par exécution, pour le reste de la liste.
        if (!reste.isEmpty()) {
            System.enqueueJob(new SyncContratsJob(reste));
        }
    }
}

public class SyncContratsFinalizer implements Finalizer {
    private final List<Id> contratIds;
    public SyncContratsFinalizer(List<Id> contratIds) { this.contratIds = contratIds; }

    public void execute(FinalizerContext context) {
        if (context.getResult() == ParentJobResult.UNHANDLED_EXCEPTION) {
            // Journaliser l'échec (objet de log, événement) pour reprise ou alerte.
            Journal.erreur('SyncContratsJob', context.getException(), contratIds);
        }
    }
}

Le Finalizer s’exécute après le job, même s’il a échoué sur une limite que try/catch ne peut pas intercepter : c’est l’endroit pour journaliser ou relancer. Les options d’enfilement ajoutent un délai et évitent les doublons :

// Délai (en minutes, 10 au plus) et signature anti-doublon : deux appels identiques ne créent qu'un job.
AsyncOptions options = new AsyncOptions();
options.MinimumQueueableDelayInMinutes = 1;
options.DuplicateSignature = QueueableDuplicateSignature.Builder()
    .addString('SyncContrats')
    .addId(agenceId)
    .build();
System.enqueueJob(new SyncContratsJob(contratIds), options);

Batch Apex : les gros volumes

Le Batch Apex découpe un grand ensemble en lots traités chacun dans une transaction séparée. start définit l’ensemble (un QueryLocator peut couvrir jusqu’à 50 millions de lignes), execute traite un lot, finish conclut.

public with sharing class RelanceEcheancesBatch implements Database.Batchable<SObject>, Database.Stateful {
    private Integer relances = 0;   // Database.Stateful : conservé d'un lot à l'autre

    public Database.QueryLocator start(Database.BatchableContext context) {
        // Requête sélective (champ date indexé) : jusqu'à 50 millions de lignes.
        return Database.getQueryLocator([
            SELECT Id, Numero__c, Date_Echeance__c, Contact__c
            FROM Contrat__c
            WHERE Statut__c = 'Actif' AND Date_Echeance__c = NEXT_N_DAYS:30
        ]);
    }

    public void execute(Database.BatchableContext context, List<Contrat__c> lot) {
        // Chaque lot est une transaction avec ses propres limites.
        List<Task> taches = new List<Task>();
        for (Contrat__c c : lot) {
            taches.add(new Task(WhatId = c.Id, WhoId = c.Contact__c, Subject = 'Relance échéance ' + c.Numero__c));
        }
        insert as user taches;
        relances += taches.size();
    }

    public void finish(Database.BatchableContext context) {
        Journal.info('RelanceEcheancesBatch', relances + ' relance(s) créée(s)');
    }
}

// Lancement : taille de lot 200 par défaut, 2 000 au plus.
Database.executeBatch(new RelanceEcheancesBatch(), 200);
  • Cinq batchs au plus s’exécutent en même temps ; les suivants attendent dans la flex queue.
  • Un lot en échec n’annule pas les autres : prévoir le journal des erreurs et la reprise.
  • La requête de start doit être sélective sur un gros objet.

Schedulable : à heure fixe

public with sharing class RelanceEcheancesPlanif implements Schedulable {
    public void execute(SchedulableContext context) {
        Database.executeBatch(new RelanceEcheancesBatch(), 200);
    }
}

// Chaque jour à 6 h (secondes minutes heures jour-du-mois mois jour-de-semaine)
System.schedule('Relance des échéances', '0 0 6 * * ?', new RelanceEcheancesPlanif());

La classe planifiée reste minimale : elle lance un batch ou un queueable, qui porte la logique. Une org accepte au plus cent tâches planifiées ; une planification se suit et se supprime dans Setup → Scheduled Jobs.

Platform Events : découpler

Un Platform Event annonce un fait (« contrat signé ») sans savoir qui l’écoute. Les abonnés (trigger Apex, flow, application externe via la Pub/Sub API) réagissent chacun dans leur transaction.

// Publication (depuis un trigger, un flow ou Apex) : l'événement part après le commit de la transaction.
EventBus.publish(new Contrat_Signe__e(Contrat_Id__c = contrat.Id, Montant__c = contrat.Prime__c));

// Abonné : trigger after insert sur l'événement, exécuté dans sa propre transaction.
trigger ContratSigneTrigger on Contrat_Signe__e (after insert) {
    List<Id> ids = new List<Id>();
    for (Contrat_Signe__e e : Trigger.new) {
        ids.add(e.Contrat_Id__c);
    }
    System.enqueueJob(new SyncContratsJob(ids));
}

Avec la publication « après commit » (comportement par défaut), l’événement ne part que si la transaction qui l’émet réussit : pas d’annonce d’un contrat dont l’enregistrement a été annulé.

Bonnes pratiques

  • Penser en lots : un traitement asynchrone reçoit souvent 200 enregistrements d’un coup ; aucune requête dans une boucle.
  • Éviter les tempêtes : un trigger qui enfile un job par enregistrement atteint vite la limite de 50 jobs par transaction. Enfiler un seul job pour tout le lot.
  • Prévoir l’échec : Finalizer, journal d’erreurs, reprise idempotente. Un traitement asynchrone échoue sans que personne ne regarde.
  • Tester : entre Test.startTest() et Test.stopTest(), les traitements asynchrones s’exécutent de façon synchrone, ce qui permet de vérifier leur résultat.
  • Superviser : Setup → Apex Jobs et l’objet AsyncApexJob donnent l’état et les erreurs de chaque job.
🍪 Gestion des cookies
Ce site utilise des cookies pour mesurer l'audience (Google Analytics) et lire des vidéos (YouTube). Vous pouvez personnaliser votre choix.
Mentions légales
Réglages des cookies
Essentiels Nécessaires au fonctionnement du site — ne peuvent pas être désactivés.
Google Analytics Analyse d'audience (pages vues, sources de trafic). Aucune donnée personnelle identifiante.
YouTube Permet de lire les vidéos YouTube intégrées sur cette page.