How to Build a Scalable Apex Trigger Framework That Won’t Break When Your Org Scales

One trigger per object. Handler classes. Bypass logic. Recursion control. This is the Apex architecture pattern that survives org growth with full code walkthroughs from the Hokoriam development team.

Most Salesforce orgs start with simple triggers. A before insert on Account. An after update on Opportunity. Each one works fine in isolation. Then the org grows, requirements compound, and suddenly you have four triggers on the same object, no one knows the execution order, DML limits are being hit in production, and every new requirement risks breaking something that was working. The Apex trigger framework pattern solves this structurally before the org reaches that point.

Why Single Triggers Without a Framework Break at Scale

Without a framework, trigger logic accumulates directly in trigger files. Business logic that should live in service classes ends up inside trigger blocks. Multiple developers add separate triggers on the same object. Recursion control becomes a mix of static Boolean flags added as afterthoughts. Testing becomes difficult because there is no separation between the trigger entry point and the business logic it executes.

The result is an org where changing one requirement requires reading and understanding three different trigger files, two of which were written by developers who left the team eighteen months ago. This is not a hypothetical. It is the default trajectory of every Salesforce org that skips architectural discipline during early development.

The trigger file contains exactly one call per execution context. No logic. No DML. No SOQL. Just method invocations on the handler class.

trigger AccountTrigger on Account (
    before insert, before update, before delete,
    after insert, after update, after delete, after undelete
) {
    // One line. That's it. All logic lives in the handler.
    AccountTriggerHandler.run();
}

1. The Handler Class – Where Logic Lives

      The handler class reads the trigger context and routes execution to the appropriate methods. Each method represents a single execution context – beforeInsert, afterUpdate, and so on. Business logic either lives directly in these methods for simple cases, or delegates to service classes for anything complex.

      public with sharing class AccountTriggerHandler {
      
          public static void run() {
              // Bypass check - allows disabling trigger in data migrations
              if (TriggerBypassManager.isBypassed('Account')) return;
      
              if (Trigger.isBefore) {
                  if (Trigger.isInsert) beforeInsert(Trigger.new);
                  if (Trigger.isUpdate) beforeUpdate(Trigger.new, Trigger.newMap, Trigger.oldMap);
                  if (Trigger.isDelete) beforeDelete(Trigger.old, Trigger.oldMap);
              }
      
              if (Trigger.isAfter) {
                  if (Trigger.isInsert) afterInsert(Trigger.new, Trigger.newMap);
                  if (Trigger.isUpdate) afterUpdate(Trigger.new, Trigger.newMap, Trigger.oldMap);
              }
          }
      
          private static void beforeInsert(List<Account> newAccounts) {
              AccountService.setDefaultRating(newAccounts);
              AccountService.validateRequiredFields(newAccounts);
          }
      
          private static void afterInsert(List<Account> newAccounts) {
              AccountService.createDefaultContacts(newAccounts);
          }
      
          private static void beforeUpdate(
              List<Account> newAccounts,
              Map<Id, Account> oldMap
          ) {
              AccountService.handleOwnerChange(newAccounts, oldMap);
          }
      
          private static void afterUpdate(
              List<Account> newAccounts,
              Map<Id, Account> oldMap
          ) {
              AccountService.syncRelatedOpportunities(newAccounts, oldMap);
          }
      
          private static void beforeDelete(List<Account> oldAccounts) {
              AccountService.preventDeleteIfActiveContracts(oldAccounts);
          }
      }

      2. The Bypass Manager – Controlled Trigger Disabling

        Every production org eventually needs to run data migrations, mass updates, or batch jobs where trigger execution is not appropriate or causes governor limit issues. Without a bypass mechanism, developers use ad-hoc static Boolean flags. With a Bypass Manager, the pattern is consistent, testable, and safe.

        public class TriggerBypassManager {
        
            // Store bypassed objects in a Set - add/remove programmatically
            private static Set<String> bypassedObjects = new Set<String>();
        
            public static void bypass(String objectName) {
                bypassedObjects.add(objectName.toLowerCase());
            }
        
            public static void clearBypass(String objectName) {
                bypassedObjects.remove(objectName.toLowerCase());
            }
        
            public static void clearAllBypasses() {
                bypassedObjects.clear();
            }
        
            public static Boolean isBypassed(String objectName) {
                return bypassedObjects.contains(objectName.toLowerCase());
            }
        }
        
        // Usage in a data migration Apex class:
        TriggerBypassManager.bypass('Account');
        // ... perform bulk operations ...
        TriggerBypassManager.clearBypass('Account');

        3. The Service Class – Business Logic Separated

        Service classes contain the actual business logic. They are plain Apex classes with no trigger context dependency, which makes them independently testable, reusable from Flows and LWC components, and easier to maintain as requirements change.

        ublic with sharing class AccountService {
        
            public static void setDefaultRating(List<Account> accounts) {
                for (Account acc : accounts) {
                    if (acc.Rating == null) {
                        acc.Rating = 'Warm';
                    }
                }
                // No DML - this is a before trigger, changes are saved automatically
            }
        
            public static void createDefaultContacts(List<Account> newAccounts) {
                List<Contact> contactsToInsert = new List<Contact>();
                for (Account acc : newAccounts) {
                    if (acc.Type == 'Prospect') {
                        contactsToInsert.add(new Contact(
                            LastName = 'Primary Contact',
                            AccountId = acc.Id
                        ));
                    }
                }
                // Bulk DML outside loop - bulkification best practice
                if (!contactsToInsert.isEmpty()) insert contactsToInsert;
            }
        
            public static void handleOwnerChange(
                List<Account> newAccounts,
                Map<Id, Account> oldMap
            ) {
                List<Account> changedAccounts = new List<Account>();
                for (Account acc : newAccounts) {
                    if (acc.OwnerId != oldMap.get(acc.Id).OwnerId) {
                        changedAccounts.add(acc);
                    }
                }
                // Only process accounts where owner actually changed
                if (!changedAccounts.isEmpty()) {
                    notifyNewOwners(changedAccounts);
                }
            }
        
            private static void notifyNewOwners(List<Account> accounts) {
                // send email / create tasks / etc.
            }
        }
        public with sharing class AccountTriggerHandler {
        
            // Track processed IDs per transaction - static resets between transactions
            private static Set<Id> processedIds = new Set<Id>();
        
            private static void afterUpdate(
                List<Account> newAccounts,
                Map<Id, Account> oldMap
            ) {
                List<Account> unprocessed = new List<Account>();
                for (Account acc : newAccounts) {
                    if (!processedIds.contains(acc.Id)) {
                        unprocessed.add(acc);
                        processedIds.add(acc.Id);
                    }
                }
                if (!unprocessed.isEmpty()) {
                    AccountService.syncRelatedOpportunities(unprocessed, oldMap);
                }
            }
        }

        How to Write Tests for This Pattern

        Because business logic lives in service classes rather than trigger files, tests can be written directly against the service class methods – no DML required for unit testing. Integration tests that fire the trigger verify the end-to-end flow. This separation makes the test suite faster and more maintainable.

        @isTest
        private class AccountServiceTest {

        @isTest
        static void testSetDefaultRating_setsWarmWhenNull() {
        List<Account> accounts = new List<Account>{
        new Account(Name='Test Co', Rating=null),
        new Account(Name='Test Co 2', Rating='Hot')
        };

        Test.startTest();
        AccountService.setDefaultRating(accounts);
        Test.stopTest();

        // Verify default was applied only where null
        System.assertEquals('Warm', accounts[0].Rating, 'Should set Warm when null');
        System.assertEquals('Hot', accounts[1].Rating, 'Should not override existing');
        }

        @isTest
        static void testBypassManager_preventsTriggerExecution() {
        TriggerBypassManager.bypass('Account');
        // Insert accounts - trigger should not fire
        insert new Account(Name='Bypass Test');
        // Assert no side effects of trigger logic occurred
        TriggerBypassManager.clearBypass('Account');
        }
        }

        The trigger framework pattern is not about complexity. It is about making the next developer who touches this code including yourself in six months able to understand, modify, and test it safely.

        Hokoriam Builds Production – Grade Apex Architecture

        With 7+ years and 100+ Salesforce projects delivered, our certified development team designs Apex trigger frameworks, service layers, and testing strategies that scale with your org. Whether you are starting fresh, refactoring legacy triggers, or need an Apex code review, we have done it across every major industry.

        Talk to Our Team →

        This site uses cookies to offer you a better browsing experience. By browsing this website, you agree to our use of cookies.