Apex Triggers : les bonnes pratiques à connaître

Les Apex Triggers restent, malgré la montée des Flows, un pilier de l'automatisation Salesforce pour toute logique complexe. Mal écrits, ils sont aussi la première cause d'incidents en production : limites gouverneurs dépassées, récursions infinies, conflits avec d'autres packages. Pour un éditeur AppExchange, où le code cohabite avec celui d'autres applications, ces règles ne sont pas optionnelles.

1. Un seul trigger par objet

Salesforce ne garantit pas l'ordre d'exécution entre plusieurs triggers définis sur le même objet. La bonne pratique, désormais un standard de l'industrie, consiste à n'avoir qu'un seul trigger par objet, qui délègue immédiatement à une classe handler :

trigger FactureTrigger on Facture__c (before insert, before update, after insert, after update) {
    FactureTriggerHandler.run(Trigger.new, Trigger.oldMap, Trigger.operationType);
}

Cette délégation permet de tester la logique métier indépendamment du contexte trigger, et évite les conflits d'ordre d'exécution avec d'éventuels triggers déclarés par d'autres packages installés dans le même org.

2. Bulkifier systématiquement

Un trigger s'exécute toujours sur une collection d'enregistrements (Trigger.new), jamais sur un seul. Toute requête SOQL ou opération DML placée dans une boucle est une erreur garantie de dépasser les limites gouverneurs dès qu'un traitement en masse (import, API bulk) déclenche le trigger sur plus de 200 enregistrements.

// A EVITER : requête dans la boucle
for (Facture__c f : Trigger.new) {
    Compte__c c = [SELECT Id FROM Compte__c WHERE Id = :f.Compte__c]; // erreur garantie
}

// CORRECT : collecte des Ids puis une seule requête groupée
Set<Id> compteIds = new Set<Id>();
for (Facture__c f : Trigger.new) {
    compteIds.add(f.Compte__c);
}
Map<Id, Compte__c> comptes = new Map<Id, Compte__c>(
    [SELECT Id, Nom__c FROM Compte__c WHERE Id IN :compteIds]
);

3. Se prémunir de la récursion

Un trigger after update qui met à jour le même objet réenclenche le trigger, pouvant provoquer une boucle infinie ou une consommation excessive de limites. Utilisez un indicateur statique pour contrôler l'exécution :

public class FactureTriggerHandler {
    private static Boolean dejaExecute = false;

    public static void run(List<Facture__c> nouvelles, Map<Id, Facture__c> anciennes, System.TriggerOperation type) {
        if (dejaExecute) return;
        dejaExecute = true;
        // logique métier
    }
}
Pour un package managé, préférez un mécanisme de contrôle de récursion réutilisable et testé plutôt qu'un booléen dupliqué dans chaque handler : centralisez cette logique dans une classe utilitaire partagée.

4. Séparer la logique métier du contexte trigger

Le handler ne devrait contenir que de l'orchestration (quelle méthode appeler selon le contexte before/after, insert/update/delete). La logique métier réelle doit résider dans des classes de service testables indépendamment :

5. Vérifier CRUD/FLS et respecter les limites partagées

Dans un contexte AppExchange, votre trigger s'exécute dans l'org du client, aux côtés d'autres automatisations. Quelques règles supplémentaires s'imposent :

Un trigger correctement architecturé — un seul par objet, bulkifié, protégé contre la récursion, avec une logique métier isolée et testée — reste l'un des marqueurs les plus fiables d'un package AppExchange mature. C'est aussi l'un des premiers points examinés lors d'un audit technique ou d'une revue par les pairs.

Besoin d'aide sur ce sujet ?

Notre équipe DevToSpace développe et publie des applications AppExchange de bout en bout, de l'idée à la Security Review.

Parler à un expert