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.
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.
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]
);
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.
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 :
Trigger.operationType.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 :
SOQL et DML non indispensables : chaque requête consomme les limites partagées avec le reste de l'org, y compris les automatisations natives du client.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.
Notre équipe DevToSpace développe et publie des applications AppExchange de bout en bout, de l'idée à la Security Review.
Parler à un expert