A helper class is a plain Apex class that holds the reusable business logic your trigger handler calls into. The handler decides “which method to call when,” and the helper actually does the work — querying records, transforming data, building the next DML. Helpers are reusable: a handler, a Queueable, an LWC controller, and a batch job can all call the same helper method.
Where helpers fit in the layered model
trigger AccountTrigger (1-2 lines: delegate to handler)
↓
AccountTriggerHandler (route by Trigger.operationType)
↓
AccountHelper / AccountService (reusable business logic)
↓
AccountSelector (SOQL queries, kept in one place)
The handler is tied to trigger contexts. The helper is not — it’s pure logic. Anything else in the codebase can call the helper without dragging trigger context with it.
Concrete example
public with sharing class AccountHelper {
// Reusable: handler calls it, batch job calls it, REST endpoint calls it
public static void recalcHealthScores(List<Account> accounts) {
for (Account a : accounts) {
Decimal score = (a.AnnualRevenue == null ? 0 : a.AnnualRevenue) / 1000;
if (a.Industry == 'Healthcare') score *= 1.1;
a.Health_Score__c = score.setScale(2);
}
}
public static Map<Id, Decimal> openCaseCounts(Set<Id> accountIds) {
Map<Id, Decimal> result = new Map<Id, Decimal>();
for (AggregateResult ar : [
SELECT AccountId acct, COUNT(Id) c
FROM Case WHERE AccountId IN :accountIds AND IsClosed = false
GROUP BY AccountId
]) {
result.put((Id) ar.get('acct'), (Decimal) ar.get('c'));
}
return result;
}
}
public with sharing class AccountTriggerHandler {
public void run() {
if (Trigger.isBefore && Trigger.isInsert) {
AccountHelper.recalcHealthScores(Trigger.new);
}
}
}
The handler is thin. The helper does the work. The helper has no dependency on Trigger.new or Trigger.isBefore — pass it a list, get a list back.
Helper vs Handler vs Service vs Selector
The terms aren’t standardized across orgs, but most teams converge on:
| Role | Knows about trigger context? | Issues SOQL? | Issues DML? | Reused across the codebase? |
|---|---|---|---|---|
| Trigger file | Yes (one line of routing) | No | No | No |
| Handler | Yes (dispatches per context) | No | No | No |
| Helper / Service | No | Sometimes | Yes | Yes |
| Selector | No | Yes (only) | No | Yes |
Salesforce’s enterprise pattern (fflib) names them more strictly: Domain, Service, Selector, Unit of Work. A helper sits between Service and a utility — same idea, less ceremony.
Why bother
- Reusability — the same
recalcHealthScoresis called from a trigger, a Queueable nightly job, and an admin button. - Testability — a helper test passes records in directly, no need to construct a trigger context.
- Single source of truth — the formula for Health Score lives in one method. When the formula changes, you change it once.
Common interview follow-ups
- Can a helper class issue DML? — Yes, that’s normal. The handler is the thing that should not contain DML inline; the helper is fine.
- Is helper the same as utility? — Close. “Utility” usually means generic, stateless helpers (date math, string manipulation). “Helper” usually means domain-specific (Account, Opportunity).
- Why not put helper logic in the handler? — Then it isn’t reusable, and the handler grows into the 1,500-line monstrosity everyone has seen at least once.
Verified against: Apex Developer Guide — Best Practices for Triggers, Trailhead — Apex Triggers module. Last reviewed 2026-05-17.