skills/sf-apex-enterprise-patterns/SKILL.md
Use when implementing Salesforce Apex Enterprise Patterns (FFLIB) — Selector, Domain, Service, Unit of Work layers. Do NOT use for simple orgs or constraints.
npx skillsauth add jiten-singh-shahi/salesforce-claude-code sf-apex-enterprise-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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 implementationsdevelopment
Update Salesforce platform reference docs with latest release features and deprecation announcements. Use when SessionStart hook warns docs are outdated or a new Salesforce release has shipped. Do NOT use for Apex or LWC development.
development
Use when syncing documentation after Salesforce Apex code changes. Update README, API docs, and deploy metadata references to match the current org codebase.
development
Use when managing context during long Salesforce Apex development sessions. Suggests manual compaction at logical intervals to preserve deploy and org context across phases.
tools
Visualforce development — pages, controllers, extensions, ViewState, JS Remoting, LWC migration. Use when maintaining VF pages, building PDFs, or planning VF-to-LWC migration. Do NOT use for LWC, Aura, or Flow.