_,-._
/ \_/ \
>-(_)-< AFF
\_/ \_/
`-'
Apex Foundation Framework
AFF is a modular framework for Salesforce Apex development that provides foundational building blocks to streamline development of enterprise applications. The framework is designed with loose coupling in mind, allowing developers to use only the components they need without bringing in unnecessary dependencies. Each module can be used independently or in combination with others, allowing developers to choose only the components they need.
Core Architecture
Executors are the fundamental building blocks of the framework. They represent context-independent units of business logic that can be invoked from:
- Triggers
- Web Services
- Batch Jobs
- Scheduled Jobs
- Queueable Jobs
- Controllers
- Any Apex context
public inherited sharing class AccountProcessor implements AFFBase.Executor {
private List<Account> accounts;
private AFFDml.ChangeUnit changeUnit;
public AccountProcessor(List<Account> accounts, AFFDml.ChangeUnit changeUnit) {
this.accounts = accounts;
this.changeUnit = changeUnit;
}
public void execute() {
// Business logic
processAccounts();
}
}
// In trigger
public with sharing class AccountTriggerHandler {
public void execute() {
new AccountProcessor(
Trigger.new,
new AFFDml.DefaultChangeUnitImpl(AFFDml.AccessMode.USER_MODE)
).execute();
}
}
// In batch
public without sharing class AccountBatch implements Database.Batchable<SObject> {
public void execute(Database.BatchableContext bc, List<Account> scope) {
new AccountProcessor(
scope,
new AFFDml.DefaultChangeUnitImpl(AFFDml.AccessMode.SYSTEM_MODE)
).execute();
}
}
// In API
@RestResource(urlMapping='/accounts/*')
global with sharing class AccountAPI {
@HttpPost
global static void processAccounts(List<Account> accounts) {
new AccountProcessor(
accounts,
new AFFDml.DefaultChangeUnitImpl(AFFDml.AccessMode.USER_MODE)
).execute();
}
}
The framework supports two primary execution patterns:
public class MyExecutor implements AFFBase.Executor {
private List<SObject> records;
public void execute() {
for(SObject record : records) {
// Process single record
}
}
}
Advantages
- Clear separation of concerns
- Easy to maintain and test
- Each executor owns it's loop
- Better error isolation
Disadvantages
- Multiple passes through the data
- More CPU time consumption
- May hit governor limits with large data sets
public inherited sharing class MyExecutor
implements AFFBase.Executor, AFFBase.DataSetter
{
private SObject record;
public AFFBase.DataSetter setData(Object data) {
this.record = (SObject)data;
return this;
}
public void execute() {
// Process single record
}
}
// Usage
for(SObject record : records) {
((AFFBase.Executor)executor1.setData(record)).execute();
((AFFBase.Executor)executor2.setData(record)).execute();
}
Advantages:
- Single pass through the data
- More CPU efficient
- Better for large data volumes
- Lower memory usage
Disadvantages:
- More complex execution flow
- Harder to debug
- Tighter coupling between executors
-
Use One Loop Per Executor when:
- Operations are independent
- Memory is not a concern
- Simple maintenance is priority
- Debugging needs are high
-
Use One Loop - Many Executors when:
- Operations are interdependent
- Processing large data volumes
- CPU time is critical
- Operations need to share context
Framework Modules
Base Module
Required Classes: AFFBase
The base module provides core interfaces and classes used throughout the framework.
---
title: Base Module Class Diagram
---
classDiagram
namespace AFFBase {
class Entity {
<<interface>>
+String name()
+Boolean equals(Object)
+Integer hashCode()
}
class Category {
<<interface>>
}
class Executor {
<<interface>>
+void execute()
}
class ExecutionManager {
<<interface>>
+ExecutionManager addExecutor(Executor)
+ExecutionManager addExecutor(Category, Executor)
+void execute()
}
class AbsExecutionManager {
<<abstract>>
#Map~Category,List~Executor~~ executors
#List~Category~ categoriesInOrder
}
class SimpleCategoryImpl {
}
}
AbsExecutionManager ..|> ExecutionManager
Category --|> Entity
SimpleCategoryImpl --|> Category
Key Components:
-
Entity: Interface for named entities
-
Executor: Interface for executable units of work
-
ExecutionManager: Interface for managing categorized executors
-
Category: Interface for categorizing executors
Trigger Module
Required Classes: AFFBase
, AFFTrigger
The trigger module provides structured trigger handling with comprehensive support for different trigger contexts.
---
title: Trigger Module Class Diagram
---
classDiagram
namespace AFFBase {
class AbsExecutionManager {
<<abstract>>
}
}
namespace AFFTrigger {
class TriggerExecutionManager {
+ExecutionManager addExecutor(Category, Executor)
+void execute()
-TriggerCategory getTriggerCategory()
}
class TriggerCategory {
+String name()
}
}
TriggerExecutionManager --|> AbsExecutionManager
trigger AccountTrigger on Account (before insert, after insert) {
new AccountTriggerHandler(
Trigger.new, Trigger.old,
Trigger.newMap, Trigger.oldMap
).execute();
}
public class AccountTriggerHandler implements AFFBase.Executor {
public void execute() {
new AFFTrigger.TriggerExecutionManager()
.addExecutor(AFFTrigger.BEFORE_INS_TRG, new Validator())
.addExecutor(AFFTrigger.BEFORE_INS_TRG, new Defaulter())
.addExecutor(AFFTrigger.AFTER_INS_TRG, new NotificationSender())
.execute();
}
}
public class AccountTriggerHandler implements AFFBase.Executor {
// ...
public void execute() {
// One Loop - Many Executors for field updates
new AccountFieldUpdaters(Trigger.new).execute();
// One Loop Per Executor for independent operations
new OpportunityCreator(Trigger.new).execute();
new TaskCreator(Trigger.new).execute();
}
}
// Field updaters using one loop - many executors
public class AccountFieldUpdaters implements AFFBase.Executor {
private List<SObject> accounts;
private List<AFFBase.Executor> fieldUpdaters;
//...
public void execute() {
for(Account acc : accounts) {
for(AFFBase.Executor updater : fieldUpdaters) {
((AFFBase.DataSetter)updater).setData(acc);
updater.execute();
}
}
}
}
The framework intentionally uses shallow copies of trigger context data:
new MyTriggerHandler(
Trigger.new, // Shallow copy
Trigger.old, // Shallow copy
Trigger.newMap, // Shallow copy
Trigger.oldMap // Shallow copy
).execute();
Why Not Immutable?
-
Performance Considerations
- Deep cloning large datasets would consume heap space
- Additional CPU time for cloning operations
- Memory constraints with governor limits
-
Intended Behavior
- Trigger handlers often need to modify records
- Changes need to propagate to the database
- Pattern follows Salesforce's native trigger behavior
-
Best Practices
- Handle data modifications in appropriate trigger contexts (before triggers for record modifications)
- Use clear execution order to prevent unexpected modifications
- Document when and where modifications occur
-
Data Modification
- Modify records only in before triggers
- Use after triggers for related object operations
- Document any record modifications
-
Execution Pattern Selection
- Use "One Loop Per Executor" for simpler, independent operations
- Use "One Loop - Many Executors" for complex, interdependent operations
- Consider data volume when choosing patterns
-
Error Handling
- Implement proper error handling in each executor
- Use feature toggles for graceful degradation
- Consider transaction scope in error scenarios
-
Performance
- Monitor CPU time and heap size
- Batch operations where possible
- Use appropriate execution pattern for data volume
-
Testing
- Test each executor independently
- Test execution order scenarios
- Verify error handling
- Test with various data volumes
Iterator Module
Required Classes: AFFIter
, AFFCommons
The Iterator module provides functional programming patterns for Apex, inspired by JavaScript's array methods. All functional operations (transform
, filter
, forEach
, etc.) are immutable by design, ensuring data consistency and preventing side effects.
---
title: Iterator Module Class Diagram
---
classDiagram
namespace AFFIter {
class FilterCallback {
<<interface>>
+Boolean execute(Object, Integer, List~Object~)
}
class MapCallback {
<<interface>>
+Object execute(Object, Integer, List~Object~)
}
class ForEachCallback {
<<interface>>
+void execute(Object, Integer, List~Object~)
}
class ReduceCallback {
<<interface>>
+Object execute(Object, Object, Integer, List~Object~)
}
class Iterator {
<<interface>>
+Boolean hasNext()
+Object next()
+Integer getCurrIndex()
+List~Object~ getCollection()
+Iterator reset()
+Iterator filter(FilterCallback)
+Iterator forEach(ForEachCallback)
+Iterator transform(MapCallback)
+Boolean every(FilterCallback)
+Object reduce(ReduceCallback)
+Object reduce(Object, ReduceCallback)
+Boolean some(FilterCallback)
}
class ListIterator {
#List~Object~ lst
#Integer currIndex
+ListIterator(List~Object~)
+Boolean hasNext()
+Object next()
+Integer getCurrIndex()
+List~Object~ getCollection()
+Iterator reset()
}
class SetIterator {
+SetIterator(Set~Object~)
}
class MapIterator {
-ListIterator listIter
+MapIterator(Map~Object,Object~)
+Boolean hasNext()
+Object next()
}
class MapEntry {
+Object key
+Object value
+MapEntry(Object, Object)
+Object clone(Boolean)
+Boolean equals(Object)
}
}
ListIterator ..|> Iterator
SetIterator --|> ListIterator
MapIterator ..|> Iterator
MapIterator o-- ListIterator
MapIterator *-- MapEntry
Iterator ..> FilterCallback : uses
Iterator ..> MapCallback : uses
Iterator ..> ForEachCallback : uses
Iterator ..> ReduceCallback : uses
// Using filter and transform
AFFIter.Iterator it = new AFFIter.ListIterator(accounts);
List<Task> tasks = (List<Task>)it
.filter(new HighValueAccountFilter())
.transform(new AccountToTaskTransformer())
.getCollection();
// Using forEach with immutable operations
it.forEach(new AccountProcessor())
.forEach(new NotificationSender());
// Filter callback example
public class HighValueAccountFilter implements AFFIter.FilterCallback {
public Boolean execute(Object item, Integer i, List<Object> arr) {
Account acc = (Account)item;
return acc.AnnualRevenue > 1000000;
}
}
// Transform callback example
public class AccountToTaskTransformer implements AFFIter.MapCallback {
public Object execute(Object item, Integer i, List<Object> arr) {
Account acc = (Account)item;
return new Task(
WhatId = acc.Id,
Subject = 'Follow up',
ActivityDate = Date.today().addDays(7)
);
}
}
All operations create new collections:
List<Account> accounts = [SELECT Id, Name FROM Account];
AFFIter.Iterator it = new AFFIter.ListIterator(accounts);
// Each operation creates new collection
Iterator filtered = it.filter(new IsActive()); // New collection
Iterator transformed = filtered.transform(new AccountWrapper()); // New collection
Iterator processed = transformed.forEach(new AccountProcessor()); // New collection
// Original iterator remains unchanged
System.assert(it.getCollection() != filtered.getCollection());
-
Leverage Immutability
- Rely on the guaranteed immutability for safe parallel processing
- Use the same iterator for multiple processing paths
- Don't worry about operations affecting source data
-
Performance Considerations
- Be aware that each operation creates new collections
- Consider the trade-off between immutability and memory usage
- Use batch processing for very large datasets
-
Chain Design
- Create clear, intention-revealing chains
- Split complex operations into meaningful steps
- Take advantage of the ability to reuse iterators
-
Testing
- Verify immutability in tests
- Test multiple processing paths
- Ensure source data remains unchanged
The Iterator module uses AFFCommons.deepClone()
for copying objects, which handles:
- SObjects using
clone(true, true, true, true)
- Custom objects implementing
AFFCommons.Cloneable
- Collections and primitive types
// Example of a custom object working with Iterator
public class MyCustomObject implements AFFCommons.Cloneable {
public String name;
public Decimal value;
public Object clone(Boolean deepClone) {
MyCustomObject cloned = new MyCustomObject();
cloned.name = this.name;
cloned.value = this.value;
return cloned;
}
}
// Will be properly cloned in Iterator operations
List<MyCustomObject> objects = new List<MyCustomObject>();
AFFIter.Iterator it = new AFFIter.ListIterator(objects);
Feature Control Module
Required Classes: AFFFeatureCtrl
, AFFBase
The Feature Control Module provides a flexible system for managing feature toggles in your Salesforce application. It allows you to enable/disable functionality dynamically based on various conditions, without deploying new code.
---
title: Feature Control Module
---
classDiagram
namespace AFFFeatureCtrl {
class Feature {
<<interface>>
+String description()
+Boolean isEnabled()
+void setEnabled(Boolean)
}
class FeatureConfig {
<<interface`>>
+List~Feature~ getFeatures()
+Feature getFeatureByName(String)
+void setFeature(Feature)
}
class FeatureConfigMgr {
+Boolean isEnabled(Category)
+FeatureConfigMgr enableFeature(Category, Boolean)
}
class FeatureEnabler {
<<interface>>
+Boolean isEnabled()
}
}
FeatureConfigMgr o--> FeatureConfig
FeatureConfigMgr o--> FeatureEnabler
FeatureConfig --> Feature
// Feature definition
public static final AFFBase.Category FEATURE_ACC_RATING =
new AFFBase.SimpleCategoryImpl('FEATURE_ACC_RATING');
// Feature configuration
AFFFeatureCtrl.FeatureConfig featureConfig = new AFFFeatureCtrl.FeatureConfigImpl();
featureConfig.setFeatures(new List<AFFFeatureCtrl.Feature>{
new AFFFeatureCtrl.FeatureImpl(
new AFF_Features_Control_Config__mdt(
DeveloperName = 'FEATURE_ACC_RATING',
IsEnabled__c = true
)
)
});
// Feature enabler for business hours
public class BusinessHoursFeatureEnabler implements AFFFeatureCtrl.FeatureEnabler {
private BusinessHours bh;
public Boolean isEnabled() {
if ( bh == null ) {
bh = [SELECT Id FROM BusinessHours WHERE IsDefault = true LIMIT 1];
}
return BusinessHours.isWithin(bh.Id, Datetime.now());
}
}
// Usage
AFFFeatureCtrl.FeatureConfigMgr featureManager = (
new AFFFeatureCtrl.FeatureConfigMgr(
featureConfig,
new List<AFFFeatureCtrl.FeatureEnabler>{
new BusinessHoursFeatureEnabler()
}
)
).execute(); // Execute feature-specific code if enabled
- Caching Feature States
public class CachedFeatureManager {
private static Map featureStateCache;
public static Boolean isEnabled(AFFBase.Category feature) {
if (featureStateCache == null) {
featureStateCache = new Map();
}
String featureName = feature.name();
if (!featureStateCache.containsKey(featureName)) {
featureStateCache.put(
featureName,
featureManager.isEnabled(feature)
);
}
return featureStateCache.get(featureName);
}
}
- Bulk Processing
public class BulkFeatureChecker {
public Map checkFeatures(Set featureNames) {
Map results = new Map();
// Bulk query feature configurations
for(AFF_Features_Control_Config__mdt config :
[SELECT DeveloperName, IsEnabled__c
FROM AFF_Features_Control_Config__mdt
WHERE DeveloperName IN :featureNames]) {
results.put(config.DeveloperName, config.IsEnabled__c);
}
return results;
}
}
Security Module
Required Classes: AFFSecurity
The Security Module provides an efficient way to check Field-Level Security (FLS) and object CRUD permissions. It uses lazy loading and caching of field descriptions to optimize performance when checking multiple fields.
---
title: Security Module Class Diagram
---
classDiagram
namespace AFFSecurity {
class FLSCrudChecker {
<<interface>>
+Boolean isAccessible()
+Boolean isCreateable()
+Boolean isUpdateable()
}
class SObjCrudChecker {
<<interface>>
+Boolean isDeletable()
}
class FieldChecker {
-List~DescribeFieldResult~ fieldsToCheck
-Boolean pIsAccessible
-Boolean pIsUpdatable
-Boolean pIsCreatable
+Boolean isAccessible()
+Boolean isCreateable()
+Boolean isUpdateable()
}
class SObjectChecker {
-SObjectType objType
-DescribeSObjectResult objDescr
+Boolean isAccessible()
+Boolean isCreateable()
+Boolean isUpdateable()
+Boolean isDeletable()
}
}
SObjCrudChecker --|> FLSCrudChecker
FieldChecker ..|> FLSCrudChecker
SObjectChecker ..|> SObjCrudChecker
// Field-level security checking
AFFSecurity.FieldChecker fieldChecker = new AFFSecurity.FieldChecker(
Account.SObjectType,
new List<String>{'Name', 'Industry', 'AnnualRevenue'}
);
if (fieldChecker.isAccessible() && fieldChecker.isUpdateable()) {
// Process fields
}
// Object-level security checking
AFFSecurity.SObjectChecker objectChecker =
new AFFSecurity.SObjectChecker(Account.SObjectType);
if (objectChecker.isCreateable()) {
// Create records
}
- Cache Checkers When Possible
public class SecurityService {
private static Map<String, AFFSecurity.FieldChecker> checkersByObject =
new Map<String, AFFSecurity.FieldChecker>();
public static AFFSecurity.FieldChecker getChecker(
SObjectType objType,
List<String> fields
) {
String key = objType + String.join(fields, ',');
if (!checkersByObject.containsKey(key)) {
checkersByObject.put(
key,
new AFFSecurity.FieldChecker(objType, fields)
);
}
return checkersByObject.get(key);
}
}
Reuse checker instances for common field combinations.
- Group Related Fields
public class AccountSecurityProfile {
private static final List<String> BASIC_FIELDS =
new List<String>{'Name', 'Phone'};
private static final List<String> FINANCIAL_FIELDS =
new List<String>{'AnnualRevenue', 'AccountNumber'};
private static AFFSecurity.FieldChecker basicChecker;
private static AFFSecurity.FieldChecker financialChecker;
public static Boolean canAccessBasicInfo() {
if (basicChecker == null) {
basicChecker = new AFFSecurity.FieldChecker(
Account.SObjectType,
BASIC_FIELDS
);
}
return basicChecker.isAccessible();
}
}
Organize field checks by functional area.
- Error Handling
public class SecureOperation {
private AFFSecurity.FieldChecker checker;
public void execute() {
try {
validateAccess();
// Perform operation
} catch (SecurityException e) {
handleSecurityError(e);
}
}
private void validateAccess() {
if (!checker.isAccessible()) {
throw new SecurityException(
'Insufficient access to fields: ' +
String.join(fields, ',')
);
}
}
}
Provide clear security error messages.
- Minimize Describe Calls
// DO THIS - one checker for multiple operations
AFFSecurity.FieldChecker checker = new AFFSecurity.FieldChecker(
Account.SObjectType,
new List<String>{'Name', 'Industry'}
);
checker.isAccessible();
checker.isUpdateable();
// NOT THIS - multiple checkers
new AFFSecurity.FieldChecker(Account.SObjectType, fields).isAccessible();
new AFFSecurity.FieldChecker(Account.SObjectType, fields).isUpdateable();
- Use Appropriate Cache Scope
public class SecurityCache {
// Transaction cache for standard operations
private static Map<String, AFFSecurity.FieldChecker> transactionCache =
new Map<String, AFFSecurity.FieldChecker>();
// Cache key generator
private static String generateKey(SObjectType objType, List<String> fields) {
return objType + String.valueOf(fields.hashCode());
}
}
- Batch Operation Optimization
public class BatchSecurityCheck {
private AFFSecurity.FieldChecker checker;
public void processRecords(List<SObject> records) {
// Check once for the batch
if (!checker.isAccessible()) {
throw new SecurityException('Insufficient access');
}
// Process records
}
}
The Security Module's lazy loading and caching mechanisms make it particularly efficient for:
- High-volume operations
- Repeated security checks
- Complex permission validations
- Batch processing
By caching field descriptions and permission results, it minimizes the overhead of security checks while maintaining proper security enforcement.
DML Module
Required Classes: AFFDml
, AFFBase
The DML Module provides structured database operations with transaction management.
---
title: DML Module Class Diagram
---
classDiagram
namespace AFFDml {
class DmlType {
-String name
+DmlType(String name)
}
class ChangeUnit {
<<interface>>
+setDmlType(DmlType type) ChangeUnit
+getRecordsByDmlType(DmlType type) List~SObject~
+clearRecordsByDmlType(DmlType type) ChangeUnit
+clearAllRecords() ChangeUnit
+addRecord(SObject obj) ChangeUnit
+addRecords(List~SObject~ objs) ChangeUnit
+applyChange() ChangeUnit
}
class DefaultChangeUnitImpl {
-Map~DmlType,List~SObject~~ dataMap
-DmlType currDmlType
-AccessMode access
+DefaultChangeUnitImpl(AccessMode access)
+insertData(List~SObject~ data)
+updateData(List~SObject~ data)
+upsertData(List~SObject~ data)
+deleteData(List~SObject~ data)
}
}
ChangeUnit <|.. DefaultChangeUnitImpl
// Create a change unit with user mode access
AFFDml.ChangeUnit cu = new AFFDml.DefaultChangeUnitImpl(AFFDml.AccessMode.USER_MODE);
// Add records to insert
cu.setDmlType(AFFDml.T_INSERT)
.addRecord(new Account(Name = 'Test Account'))
.applyChange();
AFFDml.ChangeUnit cu = new AFFDml.DefaultChangeUnitImpl(AFFDml.AccessMode.SYSTEM_MODE);
// Insert new accounts
cu.setDmlType(AFFDml.T_INSERT)
.addRecords(newAccounts);
// Update existing accounts
.setDmlType(AFFDml.T_UPDATE)
.addRecords(existingAccounts);
// Apply all changes at once
.applyChange();
AFFDml.ChangeUnit cu = new AFFDml.DefaultChangeUnitImpl(AFFDml.AccessMode.USER_MODE);
try {
cu.setDmlType(AFFDml.T_INSERT)
.addRecords(accounts)
.applyChange();
} catch (AFFDml.BadDataException e) {
// Handle error
}
Group similar DML operations together to minimize DML statements Use addRecords() for bulk operations instead of multiple addRecord() calls
Use USER_MODE when the operation should respect user permissions Use SYSTEM_MODE only when bypassing user permissions is necessary
- Always wrap DML operations in try-catch blocks
- Clear previous records using
clearRecordsByDmlType()
when reusing a change unit
- Minimize the number of
applyChange()
calls - Use getRecordsByDmlType() to verify records before applying changes
- Clear unnecessary data using
clearAllRecords()
when reusing change units
- Clear large data sets after processing using
clearAllRecords()
- Avoid storing unnecessary references to large record collections
Factory Module
Required Classes: AFFSObjFactory
The SObject Factory Module provides a flexible and secure way to create SObject records with proper field-level security checks and default value handling. It enforces compile-time type safety and supports custom field value setting strategies.
---
title: Factory Module Class Diagram
---
classDiagram
namespace AFFSObjFactory {
class Factory {
<<interface>>
+create() SObject
}
class FieldSetter {
<<interface>>
+setValue(SObject sobj, String fieldName)
}
class StandardFactory {
-List~DescribeFieldResult~ fields
-Map~String,DescribeFieldResult~ fieldsByName
-Map~String,FieldSetter~ fieldSettersByFieldName
-SObjectType sobjType
+StandardFactory(SObjectType, List~String~, Map~String,FieldSetter~)
+applyFieldLevelSecurity(List~FieldLevelSecurity~) StandardFactory
+create() SObject
}
}
Factory <|.. StandardFactory
// Custom field setter
public class AccountNameSetter implements AFFSObjFactory.FieldSetter {
public void setValue(SObject obj, String fieldName) {
obj.put(fieldName, 'Account-' + Crypto.getRandomInteger());
}
}
// Factory setup
Map<String, AFFSObjFactory.FieldSetter> fieldSetters =
new Map<String, AFFSObjFactory.FieldSetter>{
'Name' => new AccountNameSetter()
};
// Use field setter for Name and default value for Industry
AFFSObjFactory.Factory factory = new AFFSObjFactory.StandardFactory(
Account.SObjectType,
new List<String>{'Name', 'Industry'},
fieldSetters
)
// Apply field level security
.applyFieldLevelSecurity(new List<AFFSObjFactory.FieldLevelSecurity>{
AFFSObjFactory.FieldLevelSecurity.FLS_CREATE,
AFFSObjFactory.FieldLevelSecurity.FLS_READ
});
// Create SObject
Account acc = (Account)factory.create();
- Create factories once and reuse them for better performance
- Group related fields together in a single factory
- Use descriptive field setter names for better maintainability
- Always apply appropriate FLS checks based on operation context
- Cache FLS check results when creating multiple records
- Consider using system mode for batch operations when appropriate
- Handle WrongFieldException for invalid field names
- Validate required fields before creating records
- Implement proper error logging for field setter failures
- Cache describe results and field maps
- Batch create operations when possible
- Minimize the number of field setters for frequently used factories
- Keep field setters stateless when possible
- Implement field validation in setters when needed
- Use dependency injection for complex field setting logic
Commons Module
Required Classes: AFFCommons
The Commons Module provides utility functions for:
- Hash building
- Deep cloning
- Type utilities
Example Usage:
// Hash building
AFFCommons.HashBuilder hashBuilder = new AFFCommons.HashBuilder();
Integer hash = hashBuilder
.add(account.Name)
.add(account.Industry)
.hashCode();
// Deep cloning
Account clonedAccount = (Account)AFFCommons.deepClone(account);
// Type utilities
String objType = AFFCommons.getObjectType(myObject);
Type resolvedType = AFFCommons.getTypeForApexObject(namespace, myObject);
Try to avoid deepClone
for the Collections and Complex Objects which do not implement AFFCommons.Clonable
interface.
Installation
Base Framework:
- AFFBase.cls
- AFFBase.cls-meta.xml
Trigger Module:
- AFFTrigger.cls
- AFFTrigger.cls-meta.xml
Iterator Module:
- AFFIter.cls
- AFFCommons.cls
- Associated meta.xml files
Feature Control Module:
- AFFFeatureCtrl.cls
- AFFBase.cls
- Associated meta.xml files
Security Module:
- AFFSecurity.cls
- AFFSecurity.cls-meta.xml
DML Module:
- AFFDml.cls
- AFFBase.cls
- Associated meta.xml files
Factory Module:
- AFFSObjFactory.cls
- AFFSObjFactory.cls-meta.xml
Commons Module:
- AFFCommons.cls
- AFFCommons.cls-meta.xml
Demo Implementation
The framework includes complete demo implementations that showcase best practices and common patterns:
The demo implements a complete account processing system that shows how different framework modules work together to handle trigger events with feature control and proper transaction management.
---
title: AFF DEMO Sequence Diagram
---
sequenceDiagram
participant AT as AccountTrigger
participant TH as AccountTriggerHandler
participant TEM as TriggerExecutionManager
participant FCM as FeatureConfigMgr
participant BIE as BeforeInsertExecutor
participant AIE as AfterInsertExecutor
participant BUE as BeforeUpdateExecutor
participant AL as AccountLogic
participant CU as ChangeUnit
AT->>+TH: execute()
TH->>+FCM: isEnabled(FEATURE_ACCOUNT_REALTIME_LOGIC)
FCM-->>-TH: true
TH->>+TEM: new TriggerExecutionManager()
alt Before Insert Context
TH->>TEM: addExecutor(BEFORE_INS_TRG, BeforeInsertExecutor)
TEM->>+BIE: execute()
BIE->>+FCM: addExecutor(FEATURE_AUTOASSIGN_ACC_OWNER)
BIE->>FCM: addExecutor(FEATURE_ACC_RATING)
BIE->>+AL: AccountRankingProcessorWithLoop.execute()
AL-->>-BIE: Complete
BIE-->>-TEM: Complete
else After Insert Context
TH->>TEM: addExecutor(AFTER_INS_TRG, AfterInsertExecutor)
TEM->>+AIE: execute()
AIE->>+FCM: addExecutor(FEATURE_AUTOASSIGN_TASK_2_ACC)
AIE->>+AL: TaskCreatorForNewAccountsWithLoop.execute()
AL->>+CU: setDmlType(T_INSERT)
CU->>CU: addRecords(tasks)
AL->>CU: applyChange()
CU-->>-AL: Complete
AL-->>-AIE: Complete
AIE-->>-TEM: Complete
else Before Update Context
TH->>TEM: addExecutor(BEFORE_UPD_TRG, BeforeUpdateExecutor)
TEM->>+BUE: execute()
BUE->>+FCM: addExecutor(FEATURE_ACC_RATING)
BUE->>+AL: AccountRankingProcessorWithLoop.execute()
AL-->>-BUE: Complete
BUE-->>-TEM: Complete
end
TEM->>TEM: execute()
TEM-->>-TH: Complete
TH-->>-AT: Complete
Initial Flow:
- Trigger invokes AccountTriggerHandler
- Handler checks if FEATURE_ACCOUNT_REALTIME_LOGIC is enabled
- Creates TriggerExecutionManager
Context-Specific Flows:
-
Before Insert:
- Sets up owner assignment and rating calculation
- Uses feature control to conditionally execute operations
-
After Insert:
- Creates tasks for new accounts
- Uses ChangeUnit for DML operations
-
Before Update:
- Updates account ratings based on revenue changes
Transaction Management:
- Single transaction scope
- Batched DML operations
- Proper error handling
// Main toggle for all real-time account processing
public static final AFFBase.Category FEATURE_ACCOUNT_REALTIME_LOGIC =
new AFFBase.SimpleCategoryImpl('FEATURE_ACCOUNT_REALTIME_LOGIC');
// Individual feature toggles
public static final AFFBase.Category FEATURE_AUTOASSIGN_ACC_OWNER =
new AFFBase.SimpleCategoryImpl('FEATURE_AUTOASSIGN_ACC_OWNER');
public static final AFFBase.Category FEATURE_AUTOASSIGN_TASK_2_ACC =
new AFFBase.SimpleCategoryImpl('FEATURE_AUTOASSIGN_TASK_2_ACC');
public static final AFFBase.Category FEATURE_ACC_RATING =
new AFFBase.SimpleCategoryImpl('FEATURE_ACC_RATING');
Each trigger context has its own executor to handle specific business logic:
Handles field updates and validations before account creation:
public class BeforeInsertExecutor extends TriggerEventAbs {
public void execute() {
// Auto-assign account owner if feature enabled
featureConfigMgr.addExecutor(
FEATURE_AUTOASSIGN_ACC_OWNER,
new AFF_DEMO_SObjectUtils.SObjOwnerSetter_1(newAccounts)
)
// Calculate account rating based on revenue
.addExecutor(
FEATURE_ACC_RATING,
new AFF_DEMO_AccountLogic.AccountRankingProcessorWithLoop(
newAccounts,
null
)
)
.execute();
}
}
Handles related record creation after accounts are inserted:
public class AfterInsertExecutor extends TriggerEventAbs {
private AFFDml.ChangeUnit changeUnit = new AFFDml.DefaultChangeUnitImpl(
AFFDml.AccessMode.USER_MODE
);
public void execute() {
// Create follow-up tasks for new accounts if feature enabled
featureConfigMgr.addExecutor(
FEATURE_AUTOASSIGN_TASK_2_ACC,
new AFF_DEMO_AccountLogic.TaskCreatorForNewAccountsWithLoop(
newAccounts,
changeUnit
)
)
.execute();
// Save all changes in one transaction
changeUnit.applyChange();
}
}
Handles field updates when accounts are modified:
public class BeforeUpdateExecutor extends TriggerEventAbs {
public void execute() {
// Recalculate account rating if revenue changed
featureConfigMgr.addExecutor(
FEATURE_ACC_RATING,
new AFF_DEMO_AccountLogic.AccountRankingProcessorWithLoop(
newAccounts,
new Map<Id, Account>(oldAccounts)
)
)
.execute();
}
}
The demo uses AFFFeatureCtrl
to:
- Control the entire trigger execution
- Enable/disable individual features
- Apply multiple enablement conditions
Example configuration:
// Configure features via custom metadata
AFFFeatureCtrl.FeatureConfig featureConfig = new AFFFeatureCtrl.FeatureConfigImpl();
featureConfig.setFeatures(new List<AFFFeatureCtrl.Feature>{
new AFFFeatureCtrl.FeatureImpl(
new AFF_Features_Control_Config__mdt(
DeveloperName = 'FEATURE_ACC_RATING',
IsEnabled__c = true
)
)
});
// Add conditional enablers
AFFFeatureCtrl.FeatureConfigMgr featureManager =
new AFFFeatureCtrl.FeatureConfigMgr(
featureConfig,
new List<AFFFeatureCtrl.FeatureEnabler>{
new BusinessHoursFeatureEnabler()
}
);
The demo uses AFFDml.ChangeUnit
to:
- Batch DML operations
- Control execution context (User/System mode)
- Handle errors properly
Example:
// Create change unit for user mode operations
AFFDml.ChangeUnit changeUnit = new AFFDml.DefaultChangeUnitImpl(
AFFDml.AccessMode.USER_MODE
);
// Collect changes from multiple operations
changeUnit.setDmlType(AFFDml.T_INSERT)
.addRecords(tasksToCreate)
.setDmlType(AFFDml.T_UPDATE)
.addRecords(accountsToUpdate)
// Execute all changes in one transaction
.applyChange();
- Modularity: Each trigger context has its own executor
- Feature Control: Granular control over functionality
- Transaction Management: Efficient DML handling
- Maintainability: Clear separation of concerns
- Testability: Each component can be tested in isolation
- Flexibility: Easy to add/remove features or modify behavior
Testing
The framework includes comprehensive test coverage:
-
Unit Tests:
- AFFBaseTest
- AFFTriggerTest
- AFFIterTest
- AFFFeatureCtrlTest
- AFFSecurityTest
- AFFDmlTest
- AFFSObjFactoryTest
- AFFCommonsTest
-
Integration Tests:
- AFF_DEMO_AccountTriggerHandlerTest
- AFF_DEMO_AccountLogicTest
-
Mock Implementations:
- AFFMockDml
- AFFTestHelper
Best Practices
-
Module Usage:
- Use only required modules
- Keep dependencies minimal
- Follow single responsibility principle
-
Executors:
- Keep context-independent
- Choose appropriate execution pattern
- Handle errors properly
-
Security:
- Always check FLS when required
- Use appropriate DML access mode
- Validate inputs
-
Performance:
- Cache security checks
- Batch DML operations
- Use appropriate iterator patterns
- Monitor CPU and memory usage
- Fork the repository
- Create a feature branch
- Write tests for new functionality
- Submit a pull request
Apache 2.0