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 DML

35.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