Skip to main content
New tool CRON Expression Builder — preview next run times before you schedule Apex. Open the builder →
Diagram illustrating the Apex Trigger Best Practices pattern with trigger handler classes
Apex

Apex Trigger Best Practices for Scalable Salesforce Code

Writing triggers is easy. Writing them so they don't break your org is the hard part. These are the patterns I use to keep the code clean, starting with the one trigger per object rule and handler classes.

Why Apex trigger best practices matter for your sanity

If you've ever had to debug a recursive loop at 4 PM on a Friday, you know why Apex trigger best practices matter so much for keeping your sanity. Triggers are incredibly powerful, and they're also the easiest way to break an entire Salesforce org if you aren't careful. I've spent years cleaning up "trigger soup" where the logic was scattered everywhere, and trust me, it's not a fun way to spend your week.

In my experience, what separates a senior developer from a junior one is knowing how to structure code so it doesn't fall apart when the business decides to upload 50,000 records via the API. Here is how to build triggers that actually scale.

One trigger to rule them all

I'm a big believer in the "one trigger per object" rule. When you have five different triggers on the Account object, you're basically playing Russian roulette with the order of execution. Salesforce doesn't guarantee which one runs first, and that's a recipe for bugs that are impossible to track down. If you're still figuring out what an Apex trigger actually is and how it fits into the platform, just remember: keep it to one file per object.

Delegate everything to a handler class instead of putting the logic inside the trigger itself. Your trigger file stays clean and your logic gets much easier to test. Here's a simple pattern I use in almost every project:


trigger AccountTrigger on Account (before insert, before update, after insert, after update) {
    AccountTriggerHandler handler = new AccountTriggerHandler(Trigger.new, Trigger.oldMap);
    
    if (Trigger.isBefore) {
        if (Trigger.isInsert) handler.beforeInsert();
        if (Trigger.isUpdate) handler.beforeUpdate();
    } else if (Trigger.isAfter) {
        if (Trigger.isInsert) handler.afterInsert();
        if (Trigger.isUpdate) handler.afterUpdate();
    }
}

Master bulkification as part of your Apex trigger best practices

Governor limits don't care if your code works for a single record in the UI. If it fails when a Data Loader job hits it with 200 records, it's broken. You've got to stop putting SOQL queries inside "for" loops. It's the most basic mistake there is, and I still see it in enterprise orgs all the time. Use Maps and Sets to handle data in bulk.

When you're working with related data, query it once, put it in a Map, and then use that Map inside your loop. This keeps your query count low and your performance high. If you're worried about hitting asynchronous Apex limits or synchronous SOQL limits, bulkification is your best friend. For anything going to production, it's a requirement.

Always assume Trigger.new contains 200 records. If you write your logic with that mindset, you'll almost never hit a limit exception in production.

Handling recursion and complex logic

Recursion is what happens when an "after update" trigger updates the same record again, causing the trigger to fire a second time. If you don't have a guard in place, you'll hit a limit or end up with messy data. I usually use a simple static Boolean variable in my handler, or a dedicated "recursion guard" class, to check if the logic has already run for the current transaction.

Recursion isn't the only thing to think about, though. You also need to consider where your logic lives. If you have a long-running process, like a callout to an external ERP, move that to a Queueable or @future method. Keeping the trigger execution short means the user isn't stuck staring at a loading spinner for ten seconds every time they click save.

Applying Apex trigger best practices to real-world scenarios

Hardcoded IDs trip people up constantly. Please, don't do it. Use Custom Metadata Types or Custom Settings to store configuration values like Record Type IDs or integration endpoints. That lets you change behavior in production without needing a full code deployment. It's a small change that makes your life much easier during a release.

Writing tests that actually matter

We've all seen tests that just exist to hit the 75% coverage requirement. Don't be that developer. Write tests that actually verify your business logic works. Test for single records, test for a batch of 200, and test for "negative" scenarios where the data is bad. If your trigger is supposed to prevent a delete, make sure your test actually tries to delete a record and asserts that an error was thrown.

Security and sharing

By default, triggers run in "system mode," which means they ignore user permissions. That's great for some things and dangerous for others. Use the "with sharing" keyword in your handler classes unless you have a very specific reason not to. Otherwise a trigger can hand a user access to data they were never supposed to see.

Key takeaways

  • Stick to one trigger per object and use a handler class for the heavy lifting.
  • Never put SOQL or DML inside a loop. Bulkify your logic instead.
  • Use static variables to prevent recursive triggers from firing infinitely.
  • Move long-running or external integration work to asynchronous methods.
  • Avoid hardcoding IDs; use Custom Metadata instead.
  • Implement a logging framework to catch and track errors in production.

Building triggers this way might take a little more time upfront, but it saves you hours of technical debt later. These Apex trigger best practices are the foundation of a stable Salesforce environment. Your goal is to build something the next developer won't hate you for: clean code, separated logic, and tests that mean something.

Newsletter

One email every Tuesday

New guides, tool updates, and the release-note changes that break things.

No spam. Unsubscribe in one click.

Comments

Loading comments...

Leave a Comment