ApexTools: TriggerHandler base class & field-change helpers

Apex Stem Docs
Apex StemApexToolsTriggerHandlerSalesforceApex
Detailed guide to the TriggerHandler base class in ApexTools. Covers the seven hooks, the fixed Trigger.cls pattern, and the field-change detection helpers that let you narrow afterUpdate handling in a single line.

This document explains the TriggerHandler base class that ApexTools provides. For an index that includes the other tools, see the ApexTools Guide.

What You Can Do

Simply by extending the TriggerHandler base class, the entry-point processing from Triggers settles into a clean shape: "a trigger file that only declares the seven events" plus "a handler class that only writes the hooks you override". A built-in helper for "narrowing down to records where specific fields changed" lets afterUpdate filtering be written in a single line.

Inheritance: seven hooks plus andFinally

Each Handler extends the TriggerHandler base class and overrides only the hooks it needs. They are all protected virtual, so any hook you don't override does nothing. There are eight in total: seven matching the trigger events, plus andFinally, which runs last regardless of context.

HookSignature
beforeInsert(List<SObject> newRecords)
beforeUpdate(Map<Id, SObject> newMap, Map<Id, SObject> oldMap)
beforeDelete(Map<Id, SObject> deletedMap)
afterInsert(Map<Id, SObject> newMap)
afterUpdate(Map<Id, SObject> newMap, Map<Id, SObject> oldMap)
afterDelete(Map<Id, SObject> deletedMap)
afterUndelete(Map<Id, SObject> undeletedMap)
andFinally() — always called last (in any context)

Integrated With the Fixed Trigger.cls Pattern

The trigger file follows Salesforce convention and uses a fixed form. Declare all seven events and call the Handler in one line.

APEX
trigger Opportunity on Opportunity(
  before insert,
  before update,
  before delete,
  after insert,
  after update,
  after delete,
  after undelete
) {
  (new TriggerOppHandler()).execute();
}

🚨 On a custom object you cannot put __c straight into the trigger name. Apex identifiers may not contain a double underscore (Salesforce reserves it), so trigger SalesActivity__c on SalesActivity__c(...) fails to deploy with Invalid character in identifier. Give the trigger a different name (SalesActivityTrigger, for instance — match the file name too). Standard objects are fine as trigger Opportunity on Opportunity(...).

The Handler side overrides only the hooks it needs.

APEX
public with sharing class TriggerOppHandler extends TriggerHandler {
  protected override void afterInsert(Map<Id, SObject> newRecordsMap) {
    Set<Id> opportunityIds = newRecordsMap.keySet();
    (new CopyAccountIndustryToOpportunityUsecase(opportunityIds)).invoke();
  }
}

Field-Change Detection Helpers

It's a frequent case to want to narrow afterUpdate to "records where specific fields changed". The TriggerHandler base class provides helpers for exactly that.

MethodReturn TypePurpose
getUpdatedRecordsWithChangedField(SObjectField field)List<SObject>Records where a single field changed
getUpdatedRecordsWithChangedFields(List<SObjectField> fields)List<SObject>Records where any of multiple fields changed
getUpdateRecordIdsWithChangedField(SObjectField field)Set<Id>Id version of the above
getUpdateRecordIdsWithChangedFields(List<SObjectField> fields)Set<Id>Id version of the above

Example usage:

APEX
public with sharing class TriggerOppHandler extends TriggerHandler {
  protected override void afterUpdate(Map<Id, SObject> newMap, Map<Id, SObject> oldMap) {
    Set<Id> needIds = this.getUpdateRecordIdsWithChangedFields(new List<SObjectField>{
      Opportunity.AccountId,
      Opportunity.StageName
    });
    (new RegenerateCollectionUsecase(needIds)).invoke();
  }
}

The logic "do something only when a specific field changed" consolidates into a single line, and the Handler still keeps its slim "only condition checks and Usecase invocation" shape.

Other Notes

Hooks You Don't Override Just Do Nothing

The hooks on the TriggerHandler base class are all protected virtual and do nothing by default. A Handler that only wants to handle afterInsert overrides only afterInsert — no need to fill other hooks with empty methods.

When to Use andFinally

andFinally() is a hook that is always called last in any context. Use it for "processing that must always run last, regardless of the before / after type" (finalizing audit logs, wrapping up Trace, etc.). Most Handlers don't need it.