image

AI-Ready Apex Trigger Framework: Lean, Bulk-Safe, and Metadata-Driven

An AI-ready Apex Trigger Framework in Salesforce. Learn the architecture, best practices, detailed Apex code, AI prompt templates, and pitfalls to avoid. Designed for scalability, maintainability, and AI-assisted development.

Part of the AI-Augmented Salesforce Apex Frameworks series, Foundation Layer

In most Salesforce orgs, triggers are the first Apex code written. They’re also the first to turn messy. Without structure, they get brittle, hard to debug, and dangerous to change.

This post shows how to build an Apex Trigger Framework that:

  • Keeps triggers lean and predictable
  • Moves logic into reusable handler classes
  • Stays bulk-safe and recursion-safe
  • Can be toggled on/off via metadata
  • Uses clear naming and comments so AI tools can generate and refactor code safely

Why This Framework is AI-Ready

Old trigger patterns were built only for humans. They mixed routing, logic, and config in one file. This framework separates them cleanly so both humans and AI can work with it.

Key reasons it’s AI-friendly:

  • Predictable structure. Trigger → Handler → Service → Config. Easy for AI or humans to trace.
  • Metadata controls. Enable/disable triggers instantly. No deployments.
  • Safe for bulk + recursion. Guard clauses and selectors prevent governor-limit crashes.
  • Prompt-ready. Comes with tested AI prompt templates for creating methods, refactoring, and writing tests.

Other Trigger Frameworks:
Popular Apex Trigger Frameworks include 
Kevin O’Hara’s (minimal, widely adopted), Doug Ayers’ (handler class pattern with static context methods), and Andy Fawcett’s Enterprise Patterns (more layered, DDD-inspired). Each has proven itself for traditional Apex development. This AI-Ready variant builds on these strengths and makes it purpose-built for AI-assisted development.

Core Principles

  • One trigger per object. Prevents conflicts and unpredictable execution order.
  • Handlers own the logic. Triggers only route events.
  • Context-aware methods. Separate beforeInsert, afterUpdate, etc. for clarity.
  • Bulk-safe. No DML or SOQL inside loops.
  • Metadata-driven (optional). Switch triggers on/off instantly.
  • AI-friendly patterns. Clear names, consistent parameters, structured comments.

Framework Layers

  • Trigger Layer: Routing only.
  • Handler Layer: Context-specific business logic.
  • Service Layer: Shared utilities and domain methods.
  • Config Layer: Metadata toggles for runtime control.
image
AI-Ready Apex Trigger Framework — layered architecture illustrating Trigger, Handler, Service, and Config layers, each optimized for AI-assisted Salesforce development.

Apex Example: Trigger

One trigger per object, routing only:

trigger AccountTrigger on Account (
before insert, before update, before delete,
after insert, after update, after delete, after undelete
) {
if (!TriggerHandlerConfig.isEnabled('AccountTrigger')) return;
if (!RunOnce.guard('AccountTrigger')) return;

AccountTriggerHandler handler = new AccountTriggerHandler();

if (Trigger.isBefore) {
if (Trigger.isInsert) handler.beforeInsert(Trigger.new);
if (Trigger.isUpdate) handler.beforeUpdate(Trigger.new, Trigger.old);
if (Trigger.isDelete) handler.beforeDelete(Trigger.old);
}
if (Trigger.isAfter) {
if (Trigger.isInsert) handler.afterInsert(Trigger.new);
if (Trigger.isUpdate) handler.afterUpdate(Trigger.new, Trigger.old);
if (Trigger.isDelete) handler.afterDelete(Trigger.old);
if (Trigger.isUndelete) handler.afterUndelete(Trigger.new);
}
}

AI Prompt Examples

  • Generate a handler method
    “For object Case, beforeUpdate: if Status changes from ‘Closed’ to anything else, block it. Return method in AI-Ready Trigger Framework format.”
  • Refactor a trigger
    “Refactor this trigger into the AI-Ready Trigger Framework. Move logic into the handler, add recursion guard, and ensure bulk-safety.”
  • Write a bulk test class
    “Write a test class for AccountTriggerHandler covering beforeInsert defaults, beforeDelete blocking, and afterInsert async job enqueue.”

Pitfalls to Avoid

  • Multiple triggers per object
  • DML or SOQL inside loops
  • No recursion guard
  • Hardcoded IDs or field names
  • Weak or missing test coverage

Takeaway

This framework gives you a clean baseline: bulk-safe, metadata-driven, and easy for both humans and AI to extend.

For new orgs: make this your default trigger pattern. You’ll avoid recursion issues, stay governor-safe, and give AI tools a structure they can work with.

Next up in the series: Secure CRUD + FLS Wrapper (E) — bringing the same AI-ready approach to Salesforce security.

Appendix A — Full AI-Ready Trigger Framework Reference

A.1 Trigger

Minimal routing. One trigger per object.

trigger AccountTrigger on Account (
before insert, before update, before delete,
after insert, after update, after delete, after undelete
) {
if (!TriggerHandlerConfig.isEnabled('AccountTrigger')) return;
if (!RunOnce.guard('AccountTrigger')) return;

AccountTriggerHandler handler = new AccountTriggerHandler();

if (Trigger.isBefore) {
if (Trigger.isInsert) handler.beforeInsert(Trigger.new);
if (Trigger.isUpdate) handler.beforeUpdate(Trigger.new, Trigger.old);
if (Trigger.isDelete) handler.beforeDelete(Trigger.old);
}
if (Trigger.isAfter) {
if (Trigger.isInsert) handler.afterInsert(Trigger.new);
if (Trigger.isUpdate) handler.afterUpdate(Trigger.new, Trigger.old);
if (Trigger.isDelete) handler.afterDelete(Trigger.old);
if (Trigger.isUndelete) handler.afterUndelete(Trigger.new);
}
}

A.2 Recursion Guard Utility

Stops the same trigger logic from running more than once per transaction.

public class RunOnce {
private static Set<String> executed = new Set<String>();
public static Boolean guard(String key) {
if (executed.contains(key)) return false;
executed.add(key);
return true;
}
@TestVisible
static void reset() { executed.clear(); }
}

A.3 Handler Skeleton

Starting point for all trigger logic. One method per context. Bodies are empty until you add your rules.

public with sharing class AccountTriggerHandler {

// BEFORE INSERT: prepare new records (defaults, normalization, simple validation)
public void beforeInsert(List<Account> newList) { }

// BEFORE UPDATE: guard invariants, normalize fields, compare old vs new
public void beforeUpdate(List<Account> newList, List<Account> oldList) { }

// BEFORE DELETE: block deletion if business rules say so
public void beforeDelete(List<Account> oldList) { }

// AFTER INSERT: side effects (notifications, related records, integrations)
public void afterInsert(List<Account> newList) { }

// AFTER UPDATE: react to field changes (sync, recalculation, events)
public void afterUpdate(List<Account> newList, List<Account> oldList) { }

// AFTER DELETE: cleanup or downstream notifications
public void afterDelete(List<Account> oldList) { }

// AFTER UNDELETE: repair references or resume workflows
public void afterUndelete(List<Account> newList) { }
}

A.4 Drop-In Patterns

Optional snippets for common needs. Insert into the handler methods only when required.

  • Defaults (beforeInsert): Set missing values.
for (Account a : newList) {
if (String.isBlank(a.Industry)) a.Industry = 'Not Specified';
}
  • Prevent change (beforeUpdate): Protect critical fields.
for (Integer i = 0; i < newList.size(); i++) {
Account n = newList[i], o = oldList[i];
if (o.Approved__c && n.Name != o.Name) {
n.Name = o.Name;
n.addError('Name can’t change after approval.');
}
}
  • Block delete (beforeDelete): Enforce dependent record rules.
Map<Id, Integer> counts = OpportunitySelector.countOpenByAccount(
new Map<Id, Account>(oldList).keySet()
);
for (Account a : oldList) {
if ((counts.get(a.Id) != null) && counts.get(a.Id) > 0) {
a.addError('Close open Opportunities before deleting this Account.');
}
}
  • Async work (afterInsert/afterUpdate): Defer heavy or external operations.
if (!newList.isEmpty()) {
System.enqueueJob(new AccountAfterInsertJob(newList));
}
  • Change detection (afterUpdate): Trigger work only when a field actually changes.
public class FieldChange {
public static Boolean hasChanged(SObject newer, SObject older, SObjectField f) {
Object n = newer.get(f), o = older.get(f);
return (n == null && o != null) || (n != null && o == null) || (n != null && !n.equals(o));
}
}

A.5 Utility: Change Detection

Reusable helper for comparing old vs. new field values. Keeps update logic clean.

public class FieldChange {
public static Boolean hasChanged(SObject newer, SObject older, SObjectField f) {
Object n = newer.get(f), o = older.get(f);
return (n == null && o != null) || (n != null && o == null) || (n != null && !n.equals(o));
}
}

A.6 Selector Example

Centralizes SOQL. Ensures queries are bulk-safe and easy to maintain.

public with sharing class OpportunitySelector {
public static Map<Id, Integer> countOpenByAccount(Set<Id> accountIds) {
Map<Id, Integer> result = new Map<Id, Integer>();
if (accountIds == null || accountIds.isEmpty()) return result;

for (AggregateResult ar : [
SELECT AccountId a, COUNT(Id) c
FROM Opportunity
WHERE IsClosed = false AND AccountId IN :accountIds
GROUP BY AccountId
]) {
result.put((Id) ar.get('a'), (Integer) ar.get('c'));
}
return result;
}
}

A.7 Async Jobs

Queueable jobs for async operations. Use when work is heavy or must run after commit.

public with sharing class AccountAfterInsertJob implements Queueable {
private List<Id> ids;
public AccountAfterInsertJob(List<Account> accounts) {
ids = new List<Id>();
for (Account a : accounts) ids.add(a.Id);
}
public void execute(QueueableContext qc) {
AccountService.upsertWelcomeTasks(ids);
}
}

public with sharing class AccountDeltaSyncJob implements Queueable {
private List<Id> ids;
public AccountDeltaSyncJob(List<Id> changedIds) {
ids = changedIds;
}
public void execute(QueueableContext qc) {
// Sync changes
}
}

A.8 Config Layer

Uses Custom Metadata to toggle triggers on or off without deployment.

public with sharing class TriggerHandlerConfig {
public static Boolean isEnabled(String triggerApiName) {
Trigger_Config__mdt cfg = Trigger_Config__mdt.getInstance(triggerApiName);
return cfg == null ? true : cfg.Is_Active__c;
}
}

Appendix B — Expanded AI Prompt Library

  1. Generate New Handler Method

“For object Case, beforeUpdate: if Status changes from ‘Closed’ to anything else, block it. Return method in AI-Ready Trigger Framework format.”

2. Refactor Existing Trigger

“Refactor this trigger to the AI-Ready Trigger Framework, moving all logic to the handler, adding recursion guard, and ensuring bulk-safety.”

3. Bulk Test Class

“Write a test class for AccountTriggerHandler covering beforeInsert defaults, beforeDelete blocking, and afterInsert async job enqueue.”

4. Performance Audit

“Scan these classes for SOQL/DML in loops, large heap risks, or missing bulk patterns. Return table: Issue | Risk | Fix.”

5. Metadata Toggle Review

“Check that all trigger entry points exit early if TriggerHandlerConfig.isEnabled returns false.”

Resources

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top