Design Patterns & Enterprise Architecture
Popularized by Andrew Fawcett's *Salesforce Lightning Platform Enterprise Architecture* and the open-source fflib-apex-common library:
35.1 Classic patterns in Apex
Cache describe info or config once per transaction (private static Foo instance; public static Foo getInstance())
Pluggable discount/pricing/routing algorithms via interfaces
Create implementations by type or metadata (Type.forName) Fluent construction of test data or HTTP requests
Wrap complex integrations behind a simple service Wrap sObjects in classes adding behavior/UI fields (wrapper classes) Bulk state transition
Detect records whose status changed and act on them in bulk Collect DML, commit once in dependency order
35.2 Apex Enterprise Patterns (separation of concerns)
Popularized by Andrew Fawcett's *Salesforce Lightning Platform Enterprise Architecture* and the open-source fflib-apex-common library:
Entry points (LWC controllers, triggers, REST, batch, invocable)
│ thin: validate input, call services
▼
Service layer ── business processes, transaction boundaries (e.g., OpportunityService.applyDiscounts)
│
├──► Domain layer ── object-specific behavior & validation on lists of records (Opportunities)
├──► Selector layer ── all SOQL for an object, consistent fields, security (OpportunitiesSelector)
└──► Unit of Work ── registers new/dirty/deleted records; commitWork() does ordered DML35.3 Hand-rolled layers (no library)
public inherited sharing class OpportunitiesSelector {
public List<Opportunity> selectByIdWithLines(Set<Id> ids) {
return [SELECT Id, Amount, Discount__c, (SELECT Id, UnitPrice FROM OpportunityLineItems)
FROM Opportunity WHERE Id IN :ids WITH USER_MODE];
}
}
public with sharing class OpportunityService {
@TestVisible private static OpportunitiesSelector selector = new OpportunitiesSelector();
public static void applyDiscount(Set<Id> oppIds, Decimal pct) {
if (pct <= 0 || pct > 50) throw new OpportunityServiceException('Discount must be 0–50%');
List<Opportunity> opps = selector.selectByIdWithLines(oppIds);
List<OpportunityLineItem> lines = new List<OpportunityLineItem>();
for (Opportunity o : opps) {
for (OpportunityLineItem li : o.OpportunityLineItems) {
li.UnitPrice = li.UnitPrice * (1 - pct / 100);
lines.add(li);
}
}
update as user lines;
}
public class OpportunityServiceException extends Exception {}
}35.4 fflib Unit of Work
fflib also provides fflib_SObjectSelector, fflib_SObjectDomain/fflib_SObjects, fflib_QueryFactory, and ApexMocks. The library is community-maintained (apex-enterprise-patterns on GitHub); adopt it when the team and codebase are large enough to benefit from the structure.
public class Application {
public static final fflib_Application.UnitOfWorkFactory UnitOfWork =
new fflib_Application.UnitOfWorkFactory(new List<SObjectType>{
Account.SObjectType, Contact.SObjectType, Opportunity.SObjectType // parents first
});
}
fflib_ISObjectUnitOfWork uow = Application.UnitOfWork.newInstance();
Account acc = new Account(Name = 'UoW Co');
uow.registerNew(acc);
Contact con = new Contact(LastName = 'Smith');
uow.registerNew(con, Contact.AccountId, acc); // relationship resolved at commit
uow.registerDirty(existingOpp);
uow.commitWork(); // savepoint + ordered DML + rollback on error