What’s the one thing that makes a Salesforce org feel alive?
A clear, visual process that tells you exactly how records move from “new” to “closed‑won” without you having to chase down a dozen workflow rules.
If you’ve ever stared at a squiggly diagram labeled “Apex Process” and wondered, *what on earth is actually happening behind those boxes?Because of that, * you’re not alone. That's why in practice the picture is a shortcut for a whole stack of code, triggers, and async jobs that fire in a precise order. Below I break down the typical Apex‑driven flow you’ll see in most orgs, why it matters, where people trip up, and what you can actually do today to keep it running smoothly.
What Is the Apex Process Diagram Showing
When someone hands you a flowchart with boxes like Trigger → Handler → Service → Queueable → Future, they’re trying to illustrate the execution pipeline that Salesforce uses for custom server‑side logic. In plain English, it’s the path a record takes from the moment a user clicks Save to the moment every piece of custom code that needs to run has finished Turns out it matters..
Trigger Layer
The moment a DML operation (insert, update, delete, upsert) hits the database, any Apex trigger that matches the object fires. Triggers are the entry point—think of them as the “front door” of your custom logic.
Handler / Helper Classes
Good developers never cram all their business rules into the trigger itself. Instead they delegate to a handler (or “controller”) class that contains the actual logic. This keeps the trigger tiny and testable.
Service / Domain Layer
If the logic gets more complicated—multiple objects, external calls, complex calculations—it’s common to push it further into a service class. This is where you see the real business rules, often broken into methods like calculateDiscount() or applyTerritoryRules() That alone is useful..
Asynchronous Execution (Queueable, Future, Batch)
Not everything can or should run synchronously. Long‑running calls to external APIs, heavy data crunching, or anything that could push a transaction over the 10‑second limit gets offloaded to asynchronous Apex. The diagram usually shows a decision diamond: “Is async needed?” leading to Future, Queueable, or Batch Apex.
Commit & Post‑Commit Actions
Once the synchronous part finishes, Salesforce commits the transaction. After that, post‑commit events fire—things like Platform Events, Change Data Capture, or Apex callouts that were queued. The diagram often ends with a “Commit” box and a loop back to “Notify downstream systems.”
That’s the high‑level story. The real magic (or mess) is in the details That alone is useful..
Why It Matters
If you’ve ever been burned by a “record locked” error, a silent failure that left a discount unapplied, or a nightly batch that never finished, the culprit is usually a misunderstanding of this pipeline.
- Performance – Running heavy logic synchronously can push you past the governor limits, causing the whole transaction to roll back.
- Data Integrity – Mis‑ordered triggers can overwrite each other, leaving fields in an unexpected state.
- Scalability – Hard‑coding callouts in a trigger means you’ll hit the 10‑second limit the moment you have ten users saving at once.
- Maintainability – A monolithic trigger that does everything becomes a nightmare to test and refactor.
Understanding the diagram isn’t just academic; it tells you where to put the right kind of code so the system stays fast, reliable, and easy to change And it works..
How It Works (Step‑by‑Step)
Below is the typical flow you’ll see in an Apex diagram, broken into bite‑size chunks. I’ll sprinkle in code snippets so you can picture it in your head.
1. Trigger Fires
trigger OpportunityTrigger on Opportunity (before insert, before update, after insert, after update) {
if (Trigger.isBefore) {
OpportunityHandler.handleBefore(Trigger.new, Trigger.oldMap);
} else {
OpportunityHandler.handleAfter(Trigger.new, Trigger.oldMap);
}
}
What’s happening?
Salesforce detects a DML event on Opportunity and runs the trigger in the appropriate context (before vs. after). The trigger does nothing but hand off the record lists to a handler That's the part that actually makes a difference..
2. Handler Delegates
public class OpportunityHandler {
public static void handleBefore(List newOpps, Map oldMap) {
OpportunityService.validateDiscounts(newOpps);
}
public static void handleAfter(List newOpps, Map oldMap) {
if (Trigger.Still, isAfter && Trigger. isInsert) {
QueueableOpportunitySync.
*Why use a handler?*
Separating “what to do” from “when to do it” keeps the trigger thin and makes unit testing a breeze. You can call the same handler from a test class without ever firing a real trigger.
### 3. Service Executes Business Rules
```apex
public class OpportunityService {
public static void validateDiscounts(List opps) {
for (Opportunity o : opps) {
if (o.Discount__c > 0.25 && !UserInfo.getUserRoleId().contains('Sales_Management')) {
o.addError('Discount exceeds 25% for non‑managers.');
}
}
}
}
What’s the point?
All the “real” logic lives here. You can reuse validateDiscounts from other contexts (e.g., a batch job) without pulling in trigger‑specific code.
4. Decide If Asynchronous Work Is Needed
At this stage the handler checks whether any of the records need extra processing—say, pushing the opportunity to an external ERP. If yes, it enqueues a Queueable job.
5. Queueable Job Runs
public class QueueableOpportunitySync implements Queueable, Database.AllowsCallouts {
private List oppIds;
public QueueableOpportunitySync(List opps) {
oppIds = new List();
for (Opportunity o : opps) oppIds.add(o.Id);
}
public void execute(QueueableContext ctx) {
List opps = [SELECT Id, Name, Amount FROM Opportunity WHERE Id IN :oppIds];
// Imagine a callout to an ERP system here
Http http = new Http();
// ... build request, send, handle response ...
}
public static void enqueue(List opps) {
System.enqueueJob(new QueueableOpportunitySync(opps));
}
}
Key takeaways
- Queueable lets you make callouts and stay within limits (max 50 jobs per transaction).
- Because it runs after commit, the external system sees a stable record.
6. Post‑Commit Events
Once the queueable finishes, you might fire a Platform Event:
Opportunity_Changed__e event = new Opportunity_Changed__e(
OpportunityId__c = opp.Id,
ChangeType__c = 'Created'
);
EventBus.publish(event);
Subscribers (maybe a Flow or an external MuleSoft app) react instantly. The diagram usually shows an arrow looping back to “External System,” signaling that the process isn’t over until everyone’s in sync.
Common Mistakes / What Most People Get Wrong
-
Putting Callouts Directly in Triggers
A trigger that tries toHttp.send()will hit the 10‑second limit and throw aSystem.CalloutException. The fix? Offload to Queueable or Future But it adds up.. -
Ignoring Order of Execution
Salesforce runs before triggers → validation rules → after triggers → workflow → process builder → flows → assignment rules → escalation rules → roll‑up summary. If you rely on a field that a workflow updates, you’ll be looking at stale data in the after trigger. -
Hard‑Coding IDs
Using a literal record ID (e.g.,if (o.RecordTypeId == '0123A000000XYZ')) breaks sandboxes and scratch orgs. Use Custom Metadata or Custom Settings instead And it works.. -
Skipping Test Isolation
Many devs write a single test that covers everything, then add@IsTest(SeeAllData=true). That masks real‑world failures. Write focused unit tests for each handler/service method And that's really what it comes down to.. -
Over‑Queueing
Enqueueing a job for every record in a bulk insert leads to 50‑job limits being hit fast. Batch the IDs into groups of 200 or use Batch Apex for massive data loads.
Practical Tips / What Actually Works
- Keep triggers under 10 lines. Anything more belongs in a handler.
- Adopt the “Trigger‑Handler‑Service” pattern across the org. It reduces duplication and makes code reviews painless.
- Use Custom Metadata for configurable thresholds (e.g., max discount). That way you can tweak limits without a deployment.
- use Platform Events for decoupling. If an external system needs to know about a change, publish an event rather than hard‑coding a callout.
- Batch bulk‑friendly code. Always assume
Trigger.newcan contain up to 200 records. Loop over them once, collect IDs, and pass a single list to async jobs. - Run static code analysis (PMD, SonarQube). It will flag anti‑patterns like SOQL inside loops—something that shows up as a red squiggle in the diagram but is easy to miss in code.
- Monitor async jobs. Set up a scheduled Apex that queries
AsyncApexJobfor failures and sends a Slack alert. A silent failure is the worst kind of bug.
FAQ
Q: Can I call a Queueable from a Future method?
A: Yes, but it’s rarely needed. Queueable already supports callouts and chaining, so you can just chain another Queueable instead of nesting futures.
Q: What’s the difference between a Trigger and a Process Builder flow?
A: Triggers run first, are written in Apex, and give you full programmatic control. Process Builder runs later, is declarative, and can’t handle complex logic or callouts without Apex.
Q: How do I test a Queueable that makes a callout?
A: Use Test.setMock(HttpCalloutMock.class, new MyMock()) and then Test.startTest(); System.enqueueJob(new MyQueueable()); Test.stopTest();. The mock returns a fake response, letting you assert behavior Nothing fancy..
Q: Why do I sometimes see “Maximum trigger depth exceeded” errors?
A: That means a trigger indirectly caused itself to fire again (often via a field update). Break the loop by adding a static Boolean flag or moving the update to an async context.
Q: Is it safe to use @future(callout=true) for every external integration?
A: Not really. Future methods are fire‑and‑forget and can’t be chained. Queueable gives you more flexibility, better error handling, and the ability to chain jobs.
That’s the whole picture, from the moment a user hits Save to the point where downstream systems get the update.
If you ever find yourself staring at a squiggly diagram and wondering what actually runs, just remember: triggers open the door, handlers decide what to do, services do the heavy lifting, async jobs keep the house from collapsing, and events make sure everyone else knows the story.
Understanding each step lets you design faster, more reliable processes—and, more importantly, avoid the headaches that come from “just copying the diagram and hoping it works.”
Happy coding!