Guides Salesforce Apex Enterprise Patterns (FFLIB) implementation: Selector, Domain, Service, Unit of Work layers for scalable apps beyond simple orgs.
How this skill is triggered — by the user, by Claude, or both
Slash command
/salesforce-claude-code:sf-apex-enterprise-patternsThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Implementation guidance for Apex Enterprise Patterns (AEP / FFLIB). Covers the four-layer architecture, pragmatic adoption, and when NOT to use them. Constraint rules live in `sf-apex-constraints`.
Implementation guidance for Apex Enterprise Patterns (AEP / FFLIB). Covers the four-layer architecture, pragmatic adoption, and when NOT to use them. Constraint rules live in sf-apex-constraints.
Reference: @../_reference/ENTERPRISE_PATTERNS.md
The rule: introduce a layer when the absence of that layer is causing a real problem.
Trigger / Controller / API
|
Service Layer <- Transaction boundary, orchestration
|
Domain Layer <- Business rules on record collections
|
Selector Layer <- All SOQL queries
|
Unit of Work <- All DML (atomic commit)
Selectors own all SOQL queries for an object. No SOQL appears outside a Selector.
Naming: {ObjectNamePlural}Selector — e.g., AccountsSelector, OpportunitiesSelector
public with sharing class AccountsSelector {
@TestVisible
private static AccountsSelector instance;
public static AccountsSelector newInstance() {
if (instance == null) instance = new AccountsSelector();
return instance;
}
public List<Account> selectById(Set<Id> accountIds) {
return [
SELECT Id, Name, Type, OwnerId, AnnualRevenue,
Customer_Tier__c, CreditLimit__c
FROM Account WHERE Id IN :accountIds
WITH USER_MODE ORDER BY Name
];
}
public List<Account> selectWithOpenOpportunitiesById(Set<Id> accountIds) {
return [
SELECT Id, Name, AnnualRevenue, Customer_Tier__c,
(SELECT Id, Name, Amount, CloseDate, StageName
FROM Opportunities WHERE IsClosed = false
ORDER BY CloseDate ASC)
FROM Account WHERE Id IN :accountIds WITH USER_MODE
];
}
}
public with sharing class AccountsSelector extends fflib_SObjectSelector {
public static AccountsSelector newInstance() {
return (AccountsSelector) Application.Selector.newInstance(Account.SObjectType);
}
public Schema.SObjectType getSObjectType() { return Account.SObjectType; }
public List<Schema.SObjectField> getSObjectFieldList() {
return new List<Schema.SObjectField>{
Account.Id, Account.Name, Account.Type,
Account.OwnerId, Account.AnnualRevenue
};
}
public List<Account> selectById(Set<Id> accountIds) {
return (List<Account>) selectSObjectsById(accountIds);
}
}
Encapsulates all business logic for a collection of records of the same type. Replaces trigger logic.
Naming: {ObjectNamePlural} — e.g., Accounts, Opportunities
public with sharing class Accounts {
private final List<Account> records;
private final Map<Id, Account> existingRecords;
public static Accounts newInstance(List<Account> records) {
return new Accounts(records, null);
}
public static Accounts newInstance(List<Account> records, Map<Id, Account> existing) {
return new Accounts(records, existing);
}
private Accounts(List<Account> records, Map<Id, Account> existingRecords) {
this.records = records;
this.existingRecords = existingRecords;
}
public void onBeforeInsert() {
setDefaultCustomerTier();
validateRequiredFields();
}
public void onBeforeUpdate() {
validateRequiredFields();
preventDowngradingPremiumTier();
}
public void setDefaultCustomerTier() {
for (Account acc : records) {
if (String.isBlank(acc.Customer_Tier__c)) acc.Customer_Tier__c = 'Standard';
}
}
public void validateRequiredFields() {
for (Account acc : records) {
if (acc.Type == 'Customer' && String.isBlank(acc.Industry)) {
acc.Industry.addError('Industry is required for Customer account type.');
}
}
}
public void preventDowngradingPremiumTier() {
for (Account acc : records) {
Account existing = existingRecords?.get(acc.Id);
if (existing == null) continue;
if (existing.Customer_Tier__c == 'Premium'
&& acc.Customer_Tier__c != 'Premium') {
acc.Customer_Tier__c.addError(
'Premium tier downgrade requires approval.'
);
}
}
}
}
trigger AccountTrigger on Account (
before insert, before update, after insert, after update
) {
if (Trigger.isBefore && Trigger.isInsert) {
Accounts.newInstance(Trigger.new).onBeforeInsert();
} else if (Trigger.isBefore && Trigger.isUpdate) {
Accounts.newInstance(Trigger.new, Trigger.oldMap).onBeforeUpdate();
}
}
Orchestrates business processes that span multiple objects or require a full transaction boundary.
Naming: {ObjectNamePlural}Service — e.g., AccountsService
Rules:
public with sharing class AccountsService {
public static void upgradeToPremium(Set<Id> accountIds) {
List<Account> accounts = AccountsSelector.newInstance()
.selectWithOpenOpportunitiesById(accountIds);
if (accounts.isEmpty()) {
throw new UpgradeException('No accounts found for IDs: ' + accountIds);
}
// Validate
List<String> errors = validateForUpgrade(accounts);
if (!errors.isEmpty()) {
throw new UpgradeException(String.join(errors, '\n'));
}
// Build Unit of Work
fflib_ISObjectUnitOfWork uow = Application.UnitOfWork.newInstance();
for (Account acc : accounts) {
acc.Customer_Tier__c = 'Premium';
acc.CreditLimit__c = 100000.00;
uow.registerDirty(acc);
uow.registerNew(new Opportunity(
Name = acc.Name + ' - Premium Welcome',
AccountId = acc.Id,
StageName = 'Qualification',
CloseDate = Date.today().addDays(30)
));
}
uow.commitWork(); // One atomic DML transaction
}
public class UpgradeException extends Exception {}
}
Accumulates all DML operations and commits them in a single, ordered, atomic transaction.
public class SimpleUnitOfWork {
private List<SObject> toInsert = new List<SObject>();
private List<SObject> toUpdate = new List<SObject>();
private List<SObject> toDelete = new List<SObject>();
public void registerNew(SObject record) { toInsert.add(record); }
public void registerDirty(SObject record) { toUpdate.add(record); }
public void registerDeleted(SObject record) { toDelete.add(record); }
public void commitWork() {
Savepoint sp = Database.setSavepoint();
try {
if (!toInsert.isEmpty()) insert toInsert;
if (!toUpdate.isEmpty()) update toUpdate;
if (!toDelete.isEmpty()) delete toDelete;
} catch (Exception e) {
Database.rollback(sp);
throw e;
}
}
}
public class Application {
public static final fflib_Application.UnitOfWorkFactory UnitOfWork =
new fflib_Application.UnitOfWorkFactory(
new List<SObjectType>{
Account.SObjectType,
Contact.SObjectType,
Opportunity.SObjectType
}
);
public static final fflib_Application.SelectorFactory Selector =
new fflib_Application.SelectorFactory(
new Map<SObjectType, Type>{
Account.SObjectType => AccountsSelector.class,
Opportunity.SObjectType => OpportunitiesSelector.class
}
);
}
Centralize SOQL into Selectors, business processes into Services. No FFLIB dependency needed.
When trigger logic grows beyond simple field defaults, introduce the Domain layer.
When a service needs to insert/update multiple related objects, introduce UoW for atomicity.
# Clone and deploy FFLIB
git clone https://github.com/apex-enterprise-patterns/fflib-apex-common.git
git clone https://github.com/apex-enterprise-patterns/fflib-apex-mocks.git
sf project deploy start --source-dir fflib-apex-common/sfdx-source --target-org my-org
sf project deploy start --source-dir fflib-apex-mocks/sfdx-source --target-org my-org
FFLIB is typically deployed as unmanaged source code directly from the cloned repositories, not as a versioned managed package.
sf-review-agent, sf-architect — For interactive guidancesf-apex-constraints — Governs all Apex code including enterprise pattern implementationsnpx claudepluginhub jiten-singh-shahi/salesforce-claude-code --plugin salesforce-claude-codeGenerates production-grade Apex classes with Service-Selector-Domain layering, sharing models, and async patterns (Queueable, Batchable, Schedulable). Static code generation without org connection.
Generates, refactors, and reviews Apex classes including service, selector, domain, triggers, batch, queueable, and REST resources. Includes test generation.
Provides expert patterns for Salesforce platform development including Lightning Web Components, Apex triggers, REST/Bulk APIs, Connected Apps, and Salesforce DX with scratch orgs and 2GP.