One of the most common questions in Salesforce development is: "Should I solve this with a trigger or an Apex class?" The answer depends on the scenario. Let's explore both with real-world examples.
Triggers automatically execute code when a DML event (insert, update, delete, undelete) happens on a record.
Requirement: When an Opportunity is won, automatically update the related Account's rating to "Hot".
trigger OpportunityTrigger on Opportunity (after update) {
if (Trigger.isAfter && Trigger.isUpdate) {
OpportunityTriggerHandler.updateAccountRating(Trigger.new, Trigger.oldMap);
}
}
public class OpportunityTriggerHandler {
public static void updateAccountRating(List<Opportunity> newOpps, Map<Id, Opportunity> oldMap) {
Set<Id> accountIds = new Set<Id>();
for (Opportunity opp : newOpps) {
if (opp.StageName == 'Closed Won' && oldMap.get(opp.Id).StageName != 'Closed Won') {
accountIds.add(opp.AccountId);
}
}
List<Account> accountsToUpdate = new List<Account>();
for (Account acc : [SELECT Id FROM Account WHERE Id IN :accountIds]) {
acc.Rating = 'Hot';
accountsToUpdate.add(acc);
}
update accountsToUpdate;
}
}
Apex Classes contain reusable logic that can be called from triggers, API endpoints, or scheduled jobs.
Requirement: When a lead is converted, send an email to the accountowner. You also want to run this from a custom button and a scheduled job.
// Reusable Apex Class
public class LeadConversionNotifier {
public static void notifyAccountOwner(Set<Id> accountIds) {
List<Account> accounts = [SELECT Id, OwnerId, Owner.Email FROM Account WHERE Id IN :accountIds];
List<Messaging.SingleEmailMessage> emails = new List<Messaging.SingleEmailMessage>();
for (Account acc : accounts) {
Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
mail.setTargetObjectId(acc.OwnerId);
mail.setSubject('New Lead Converted');
mail.setPlainTextBody('A new lead has been converted to an account.');
emails.add(mail);
}
if (!emails.isEmpty()) {
Messaging.sendEmail(emails);
}
}
}
// Called from trigger
trigger LeadTrigger on Lead (after update) {
if (Trigger.isAfter && Trigger.isUpdate) {
Set<Id> convertedAccounts = new Set<Id>();
for (Lead lead : Trigger.new) {
if (lead.IsConverted && !Trigger.oldMap.get(lead.Id).IsConverted) {
convertedAccounts.add(lead.ConvertedAccountId);
}
}
if (!convertedAccounts.isEmpty()) {
LeadConversionNotifier.notifyAccountOwner(convertedAccounts);
}
}
}
// Called from custom button/scheduled job
global class LeadConversionBatch implements Schedulable {
global void execute(SchedulableContext ctx) {
Set<Id> recentlyConverted = new Set<Id>();
// ... fetch recently converted leads
LeadConversionNotifier.notifyAccountOwner(recentlyConverted);
}
}
| Feature | Trigger | Apex Class |
|---|---|---|
| Auto-executes on DML | ✅ Yes | ❌ No |
| Manually invoked | ❌ No | ✅ Yes |
| Used for multiple objects | ❌ No (one per object) | ✅ Yes (reusable) |
| Testable | ✅ Yes | ✅ Yes |
Thin Trigger + Fat Class: Keep triggers thin, delegate business logic to reusable classes.
// GOOD: Trigger delegates to handler class
trigger AccountTrigger on Account (before insert, before update, after insert, after update) {
if (Trigger.isBefore) {
AccountHandler.beforeSave(Trigger.new, Trigger.oldMap);
}
if (Trigger.isAfter) {
AccountHandler.afterSave(Trigger.new, Trigger.oldMap);
}
}