Change log header
Every Apex file opens with a change log. Append a line per change; do not rewrite history.
/**
* ProblemTriggerHandler
*
* Change log
* ----------
* 2026-09-22 DS Initial version — routes Problem__c trigger events. US-12345
* 2026-09-30 LK Skip closed problems in the after-update path. BUG-2291
*/
public with sharing class ProblemTriggerHandler {
Date, name or initials, description of the change, and the user story or bug number. The ticket reference is the part people actually need six months later.
Trigger skeleton
One trigger per object, no logic, all 7 events declared — Trigger Hook depends on the full set being present even where a handler ignores some.
trigger ProblemTrigger on Problem__c (
before insert, before update, before delete,
after insert, after update, after delete, after undelete
) {
new TriggerTemplateV2().run(new ProblemTriggerHandler());
}
Bypass, per handler
Bypass logic sits in the trigger and is keyed per handler class, so one piece of automation can be switched off without disabling the object.
trigger ProblemTrigger on Problem__c (
before insert, before update, before delete,
after insert, after update, after delete, after undelete
) {
TriggerTemplateV2 template = new TriggerTemplateV2();
template.run(new ProblemTriggerHandler());
template.run(new ProblemNotificationHandler()); // disable this one alone when needed
}
Filtered scope, never raw Trigger.new
Do not hand Trigger.new to a handler method. Filter to the records that piece of logic actually operates on and pass those — the handler then states its own scope, and bulk behaviour stays predictable.
public void afterUpdate(List<Problem__c> records, Map<Id, Problem__c> oldMap) {
List<Problem__c> newlyClosed = new List<Problem__c>();
for (Problem__c record : records) {
if (record.Status__c == 'Closed' && oldMap.get(record.Id).Status__c != 'Closed') {
newlyClosed.add(record);
}
}
if (!newlyClosed.isEmpty()) {
ProblemClosureService.handleClosure(newlyClosed);
}
}
Record type Ids
Through Utils, held in a static final so the describe happens once per transaction.
private static final Id DIGITAL_SUPPORT_RT = Utils.getRecordTypeId('Case:Digital_Support');
Never a hard-coded Id — they differ per org — and never an inline describe call in a loop.
Async work
Use the org's Async class so execution context is handled for you rather than reasoned about at each call site.
Async.run(new ProblemSyncJob(problemIds));
Never call an async method from inside a loop: each iteration consumes one of the transaction's async invocations.
Credentials
Callout credentials live in Credential__c or a Named Credential — never in code, a static resource or a hard-coded string.
Prefer a Named Credential whenever the callout can use one; the platform then holds the secret and it never reaches Apex. Where Credential__c is used, remember it is ordinary data:
- store the secret in an encrypted field;
- restrict field-level security to the integration user that needs it;
- keep the records out of change sets and packages — they are environment data, seeded per org.
DML
Use Database methods so partial success is a deliberate choice, and inspect what comes back.
List<Database.SaveResult> results = Database.update(records, false);
for (Database.SaveResult result : results) {
if (!result.isSuccess()) {
Logger.error('Problem update failed', result.getErrors());
}
}
Sharing
Every class states its sharing model explicitly — with sharing by default, without sharing only with a comment explaining why. A class with no declaration inherits unpredictably depending on its caller.