Every Salesforce developer hits governor limits at some point. It usually happens at the worst possible time — production, Friday afternoon, right after a deployment. But here's the thing: governor limits aren't Salesforce being difficult. They're guardrails that force you to write code that works at scale.
Here are the patterns I use on every project to make sure Apex code doesn't fall over when data volumes grow.
Bulkification: The Non-Negotiable
This is day-one stuff, but I still see triggers that process records one at a time in production orgs. If your trigger starts with Trigger.new[0], we need to talk.
// Bad - processes one record
trigger AccountTrigger on Account (before insert) {
Account acc = Trigger.new[0];
acc.Description = 'Updated';
}
// Good - handles any number of records
trigger AccountTrigger on Account (before insert) {
for (Account acc : Trigger.new) {
acc.Description = 'Updated';
}
}
This extends to SOQL and DML too. Queries inside loops are the number one cause of limit exceptions I see in code reviews.
The Trigger Handler Pattern
One trigger per object, one handler class. No exceptions.
trigger AccountTrigger on Account (
before insert, before update, before delete,
after insert, after update, after delete, after undelete
) {
new AccountTriggerHandler().run();
}
The handler class separates concerns:
public class AccountTriggerHandler extends TriggerHandler {
public override void beforeInsert() {
AccountService.setDefaults(Trigger.new);
}
public override void afterUpdate() {
AccountService.syncToExternalSystem(
Trigger.new, Trigger.oldMap
);
}
}
This keeps your trigger file clean and makes the handler testable.
Selector Pattern for SOQL
Stop scattering SOQL queries across your codebase. Centralise them.
public class AccountSelector {
public static List<Account> getByIds(Set<Id> accountIds) {
return [
SELECT Id, Name, Industry, AnnualRevenue,
(SELECT Id, LastName FROM Contacts)
FROM Account
WHERE Id IN :accountIds
];
}
public static List<Account> getActiveByIndustry(String industry) {
return [
SELECT Id, Name, AnnualRevenue
FROM Account
WHERE Industry = :industry
AND IsActive__c = true
];
}
}
Benefits: consistent field lists, easier to optimise, single place to add new fields when needed.
Service Layer for Business Logic
Keep business logic out of triggers, controllers, and batch classes. Put it in service classes.
public class OpportunityService {
public static void closeWon(List<Opportunity> opps) {
List<Task> followUps = new List<Task>();
for (Opportunity opp : opps) {
opp.StageName = 'Closed Won';
opp.CloseDate = Date.today();
followUps.add(new Task(
WhatId = opp.Id,
Subject = 'Post-sale onboarding',
ActivityDate = Date.today().addDays(3)
));
}
update opps;
insert followUps;
}
}
Now this logic can be called from a trigger, a batch job, a REST endpoint, or an LWC — without duplication.
Queueable for Async Processing
When you need to do work that doesn't fit in a synchronous transaction — callouts, heavy processing, or chaining multiple operations — use Queueable.
public class SyncAccountsToERP implements Queueable, Database.AllowsCallouts {
private List<Id> accountIds;
public SyncAccountsToERP(List<Id> accountIds) {
this.accountIds = accountIds;
}
public void execute(QueueableContext ctx) {
List<Account> accounts = AccountSelector.getByIds(
new Set<Id>(accountIds)
);
for (Account acc : accounts) {
ERPService.sync(acc); // HTTP callout
}
}
}
Queueable is more flexible than @future — you can chain jobs, pass complex types, and monitor via AsyncApexJob.
Unit of Work Pattern for DML
When a single operation needs to insert/update multiple object types, manage DML operations together.
public class UnitOfWork {
private List<SObject> toInsert = new List<SObject>();
private List<SObject> toUpdate = new List<SObject>();
public void registerNew(SObject record) {
toInsert.add(record);
}
public void registerDirty(SObject record) {
toUpdate.add(record);
}
public void commitWork() {
insert toInsert;
update toUpdate;
}
}
This reduces DML statements and makes it easier to handle errors across related records.
The Takeaway
None of these patterns are revolutionary. They're established software engineering practices applied to the Salesforce platform. The key is using them consistently from the start, not retrofitting them when things break at scale.
If your Salesforce org is hitting limits or your codebase is becoming unmanageable, let's have a conversation about getting it sorted.
Need Help With This?
If this article resonated with a challenge you're facing, let's chat. Free 30-minute call, no obligation.