Skip to main content

Command Palette

Search for a command to run...

Complete Apex Trigger Notes

Published
•8 min read•View as Markdown

This document provides a comprehensive overview of Apex Triggers in Salesforce, covering their purpose, types, best practices, and advanced topics.

1. What is an Apex Trigger?

An Apex Trigger is code that automatically runs before or after specific database events (DML) like:

  • INSERT

  • UPDATE

  • DELETE

  • MERGE

  • UNDELETE

Use Cases:

  • Automate business processes.

  • Validate or modify data before saving.

  • Create related records.

  • Call external systems after saving.

  • Maintain data consistency.

2. Types of Triggers

Before Triggers

  • Run before the data is saved to the database.

  • Best for:

    • Validations.

    • Changing values on the record before commit.

After Triggers

  • Run after records are committed.

  • Best for:

    • Using record IDs.

    • Creating related records.

    • Callouts (via asynchronous methods).

3. Trigger Events

EventDescription
before insertBefore new records inserted.
before updateBefore existing records updated.
before deleteBefore records deleted.
after insertAfter new records inserted.
after updateAfter existing records updated.
after deleteAfter records deleted.
after undeleteAfter records restored from Recycle Bin.

4. Trigger Syntax

Basic structure:

trigger TriggerName on ObjectName (events) {
    // logic
}

Example:

trigger AccountTrigger on Account (before insert) {
    for (Account acc : Trigger.new) {
        acc.Description = 'Updated by Trigger';
    }
}

5. Trigger Context Variables

Salesforce provides helpful context variables:

Context VariableDescription
Trigger.newNew versions of records.
Trigger.oldOld versions of records.
Trigger.newMapMap of IDs → new records.
Trigger.oldMapMap of IDs → old records.
Trigger.isInsertTrue if fired on insert.
Trigger.isUpdateTrue if fired on update.
Trigger.isDeleteTrue if fired on delete.
Trigger.isBeforeTrue if a before trigger.
Trigger.isAfterTrue if an after trigger.
Trigger.isUndeleteTrue if fired on undelete.

6. Trigger Best Practices

  • ✅ One Trigger Per Object:

    • Avoid multiple triggers per object for maintainability.
  • ✅ Logic-less Trigger:

    • Move logic into a separate handler class.
  • ✅ Bulk-safe:

    • Always loop over Trigger.new.

    • Never assume only one record.

  • ✅ Avoid DML or SOQL inside loops:

    • Collect records and DML them once outside the loop.
  • ✅ Avoid recursion:

    • Use static flags to prevent infinite loops.
  • ✅ Use context variables:

    • Example:

        if (Trigger.isBefore && Trigger.isInsert) {
            // logic here
        }
      

7. Trigger Handler Pattern

Separate your logic into handler classes for better organization.

Trigger:

trigger AccountTrigger on Account (before insert) {
    AccountTriggerHandler.handleBeforeInsert(Trigger.new);
}

Handler class:

public class AccountTriggerHandler {
    public static void handleBeforeInsert(List<Account> newList) {
        for (Account acc : newList) {
            acc.Description = 'Handled by Trigger Handler';
        }
    }
}

8. Bulkification Example

Non-bulkified:

trigger BadTrigger on Contact (before insert) {
    for (Contact con : Trigger.new) {
        Account acc = [SELECT Name FROM Account WHERE Id = :con.AccountId]; // SOQL inside loop!
        con.Description__c = acc.Name;
    }
}

Bulkified:

trigger GoodTrigger on Contact (before insert) {
    Set<Id> accountIds = new Set<Id>();
    for (Contact con : Trigger.new) {
        if (con.AccountId != null) {
            accountIds.add(con.AccountId);
        }
    }
    // SOQL outside loop
    Map<Id, Account> accMap = new Map<Id, Account>(
        [SELECT Name FROM Account WHERE Id IN :accountIds]
    );
    for (Contact con : Trigger.new) {
        if (accMap.containsKey(con.AccountId)) {
            con.Description__c = accMap.get(con.AccountId).Name;
        }
    }
}

9. Trigger Order of Execution

Salesforce executes:

  1. Load old record (for updates)

  2. Run validation rules

  3. Execute before triggers

  4. Duplicate rules

  5. Save record (but not committed)

  6. Execute after triggers

  7. Assignment rules

  8. Auto-response rules

  9. Workflow rules

  10. Process Builder

  11. Escalation rules

  12. Commit record

  13. Post-commit logic (e.g. async jobs)

10. Test Class for Triggers

Example:

@isTest
private class AccountTriggerTest {
    @isTest static void testInsert() {
        // Prepare test data
        Account acc = new Account(Name='Test Account');
        insert acc; // This DML operation fires the trigger

        // Verify the trigger's effect
        Account result = [SELECT Description FROM Account WHERE Id = :acc.Id];
        System.assertEquals('Handled by Trigger Handler', result.Description);
    }
}

11. Governor Limits

Key limits:

  • 150 DML statements/transaction

  • 100 SOQL queries/transaction

  • 50,000 records retrieved via SOQL

  • Max CPU time: 10,000 ms

Write bulk-safe code to avoid limits!

12. Real-Life Trigger Scenarios

  • ✅ Set default field values

  • ✅ Prevent field changes under certain conditions

  • ✅ Create related child records

  • ✅ Roll up custom summaries

  • ✅ Notify users of changes

  • ✅ Sync related objects

  • ✅ Call external systems post-commit

13. Advanced Topics

✅ Future Methods & Queueable from Triggers

Why?

  • Triggers can’t do callouts directly.

  • Offload heavy logic.

  • Prevent hitting governor limits.

Future Methods

  • Annotated with @future.

  • No return values.

  • Primitive parameters only.

Example:

@future(callout=true)
public static void sendCallout(String accountId) {
    HttpRequest req = new HttpRequest();
    req.setEndpoint('https://example.com/api');
    req.setMethod('POST');
    req.setBody('Account Id=' + accountId);
    Http http = new Http();
    HttpResponse res = http.send(req);
}

Trigger:

trigger AccountTrigger on Account (after insert) {
    for (Account acc : Trigger.new) {
        MyFutureClass.sendCallout(acc.Id);
    }
}

Queueable Apex

  • More powerful:

    • Allows complex logic.

    • Supports non-primitive parameters.

    • Chaining jobs.

Example:

public class AccountQueueable implements Queueable {
    private List<Account> accounts;

    public AccountQueueable(List<Account> accounts) {
        this.accounts = accounts;
    }

    public void execute(QueueableContext context) {
        for (Account acc : accounts) {
            acc.Description = 'Updated by Queueable';
        }
        update accounts;
    }
}

Trigger:

trigger AccountTrigger on Account (after insert) {
    System.enqueueJob(new AccountQueueable(Trigger.new));
}

✅ Trigger Frameworks (e.g. fflib)

Benefits:

  • Standardized pattern.

  • Centralized logic.

  • Manages recursion.

  • Easy to test.

Example (fflib):

trigger AccountTrigger on Account (before insert, before update) {
    fflib_SObjectDomain.triggerHandler(AccountDomain.class);
}

public with sharing class AccountDomain extends fflib_SObjectDomain {
    public AccountDomain(List<Account> records) {
        super(records);
    }

    public override void onBeforeInsert() {
        for (Account acc : (List<Account>) Records) {
            acc.Description = 'Set by fflib';
        }
    }
}

✅ Custom Metadata for Configurable Triggers

Instead of hard-coding logic, store values in Custom Metadata.

Example:

Custom Metadata Country_Config__mdt

Country__cDefault_Type__c
USCustomer
IndiaPartner

Trigger logic:

Map<String, Country_Config__mdt> configs = new Map<String, Country_Config__mdt>(
    [SELECT Country__c, Default_Type__c FROM Country_Config__mdt]
);

for (Account acc : Trigger.new) {
    if (configs.containsKey(acc.BillingCountry)) {
        acc.Type = configs.get(acc.BillingCountry).Default_Type__c;
    }
}

✅ Error Handling in Triggers

Blocking Save

Use addError():

for (Account acc : Trigger.new) {
    if (acc.Name == 'Test') {
        acc.addError('Account name cannot be Test.');
    }
}

Partial Save

Use Database.insert with allOrNone set to false:

Database.SaveResult[] results =
    Database.insert(accList, false); // false allows partial success

for (Database.SaveResult sr : results) {
    if (!sr.isSuccess()) {
        for (Database.Error err : sr.getErrors()) {
            System.debug(err.getMessage()); // Log errors for failed records
        }
    }
}

✅ Handling Recursion

Problem: Trigger fires → update record → fires trigger again → infinite loop.

Solution: Use a static variable:

public class AccountTriggerHandler {
    public static Boolean alreadyExecuted = false;

    public static void handleBeforeInsert(List<Account> accList) {
        if (alreadyExecuted) return; // Exit if already executed in this transaction
        alreadyExecuted = true;      // Set flag to true

        for (Account acc : accList) {
            acc.Description = 'Handled only once';
        }
    }
}

✅ Platform Events from Triggers

Platform Events = Salesforce’s pub/sub. Use them to notify external systems.

Example: Platform Event: Order_Event__e

Trigger:

trigger OrderTrigger on Order (after insert) {
    List<Order_Event__e> events = new List<Order_Event__e>();
    for (Order ord : Trigger.new) {
        events.add(new Order_Event__e(
            OrderId__c = ord.Id,
            Status__c = ord.Status
        ));
    }
    EventBus.publish(events); // Publish the events
}

✅ Using Custom Settings/Metadata for Trigger Control

Why?

  • Disable triggers in certain environments.

  • Turn logic on/off without deployment.

Object_Name__cIs_Active__c
Accounttrue

Logic:

Trigger_Control__c setting = [
    SELECT Is_Active__c
    FROM Trigger_Control__c
    WHERE Object_Name__c = 'Account'
    LIMIT 1
];

if (!setting.Is_Active__c) {
    return; // Exit trigger execution if not active
}

✅ Dynamic SOQL in Triggers

Build flexible SOQL at runtime:

String fieldName = 'Name';
String query = 'SELECT ' + fieldName + ' FROM Account WHERE Id = :accId';
List<SObject> results = Database.query(query);

Be cautious:

  • Never concatenate user input into SOQL → avoid SOQL injection.

✅ Callouts from Triggers

The Problem:

  • Triggers can’t make callouts synchronously.

Solution: Use @future or Queueable:

@future(callout=true)
public static void sendCallout(String accId) {
    HttpRequest req = new HttpRequest();
    req.setEndpoint('https://example.com/api');
    req.setMethod('POST');
    req.setBody('Account Id=' + accId);
    Http http = new Http();
    HttpResponse res = http.send(req);
}

When to Use Future vs Queueable

FeatureFutureQueueable
Primitive args✔✔
Complex types✘✔
Chaining jobs✘✔
Simplicity✔✘

For heavy logic → Queueable is best.

✅ Apex Trigger Learning Path

Follow these steps to go from beginner → expert:

  • ✔ Understand syntax & context variables

  • ✔ Learn bulk-safe patterns

  • ✔ Practice using handler classes

  • ✔ Master test classes

  • ✔ Prevent recursion

  • ✔ Study trigger frameworks

  • ✔ Learn asynchronous methods

  • ✔ Explore real projects & integrations