agentleFS
grzmolgrzmol/vibe-force/skills/sf-fflib-foundations/SKILL.md

sf-fflib-foundations

Skill2 starsChanged 2 months agoMITView on GitHub

Covers the fflib Apex Enterprise Patterns stack (fflib-apex-common, fflib-apex-mocks) - the four-layer Service/Domain/Selector/Unit of Work separation, the Application factory class with its SObject-to-class binding maps, interface plus implementation naming, installation by vendoring or git submodule, sfdx-project.json and .forceignore wiring for a dedicated fflib package directory, the setMock injection points used by unit tests, PMD/Code Analyzer ruleset scoping for fflib code, and an honest decision table for when plain trigger handlers plus service classes are the better answer. Use it when a project adds or removes fflib, when creating or editing Application.cls, when deciding whether a requirement deserves a Service/Domain/Selector split, when migrating legacy Apex to Apex Enterprise Patterns, or when fflib_Application.DeveloperException, "No implementation registered for service interface", or "Selector class not found for SObjectType" appears in a log.

The file

We split this skill into the rules it gives an agent

1 rule. About 0 are common, 0 are distinctive, and 1 is very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈0

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Keep the legacy class as a thin delegator until every caller is migrated, then delete it in the same release.

    line 213

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

fflib Foundations: Apex Enterprise Patterns

Source of truth for every signature below: apex-enterprise-patterns/fflib-apex-common @ master commit dab5977 (sfdx-source/apex-common/main/classes/*.cls), fflib-apex-mocks @ master commit d81e9e1, fflib-apex-common-samplecode @ master commit 4657d63. Anything not read from those files this session is marked [unverified].

When to use

SituationUse this skill
Adding fflib to a repo for the first timeYes - references/install-and-layout.md
Writing or fixing Application.clsYes - references/application-factory.md
Choosing fflib vs trigger handler + service classYes - references/fflib-vs-plain-apex.md
Looking up what an fflib_* class doesYes - references/class-catalogue.md
Writing a selector or a querySkill sf-fflib-selector-layer
Writing a domain, service, or Unit of WorkSkill sf-fflib-domain-service-uow
Mocking, stubbing, verifying with ApexMocksSkill sf-fflib-testing
Trigger wiring, batch entry points, day-2 operationsSkill sf-fflib-operations
Plain Apex idioms, no fflib in the repoSkill sf-apex-development

Decision table: does fflib earn its keep here?

SignalfflibPlain trigger handler + service classes
SObjects with non-trivial write logic> 5<= 5
Apex classes in the package> 40< 40
Multiple entry points per SObject (trigger, LWC, REST, batch, Flow invocable)YesOne or two
Unit tests must run without DML (Set<Id> + stubs)RequiredNice to have
Second-generation packaging with a shared base libraryYesNo
Team has used Apex Enterprise Patterns beforeYesNo, and no budget to learn
Automation mostly Flow / Record-Triggered FlowNoYes - see skill sf-flow-automation
Org is a single-admin org with < 10 Apex classesNoYes

Two or more "plain" answers means do not adopt fflib. The library costs roughly 3 extra files per SObject (interface, implementation, mock registration) and a permanent onboarding tax. See references/fflib-vs-plain-apex.md for the same requirement implemented both ways.

Core patterns

1. The four layers and the dependency rules

Entry points        triggers, LWC @AuraEnabled controllers, REST resources,
                    Batchable/Queueable, InvocableMethod
      |  (only calls)
Service layer       coarse-grained, bulkified, transaction boundary, owns the UnitOfWork
      |
Domain layer        record-oriented behaviour + validation for one SObjectType
      |
Selector layer      all SOQL
      |
Unit of Work        all DML, ordered by SObjectType dependency

Enforceable rules, in priority order:

RuleWhy
Only the Service layer calls commitWork()one transaction boundary per use case
Selectors never call services or domainsthey would become untestable and recursive
Domains never issue SOQL or DML directlythey get records in, register work out
Entry points contain no business logic, only marshallinglets Flow/LWC/REST share behaviour
Every layer is reached through Application.*, never new Impl() in product codemock injection
Service methods take and return bulk shapes (Set<Id>, List<SObject>)see skill sf-governor-limits

2. Application.cls is the only wiring file

One class per package, four static finals. Verified shape from fflib-apex-common-samplecode/.../main/classes/Application.cls:

public class Application {
    // SObjectTypes in dependency order: parents before children.
    public static final fflib_Application.UnitOfWorkFactory UnitOfWork =
        new fflib_Application.UnitOfWorkFactory(
            new List<SObjectType> {
                Account.SObjectType,
                Opportunity.SObjectType,
                OpportunityLineItem.SObjectType,
                Invoice__c.SObjectType,
                InvoiceLine__c.SObjectType });

    // Interface -> implementing class.
    public static final fflib_Application.ServiceFactory Service =
        new fflib_Application.ServiceFactory(
            new Map<Type, Type> {
                IAccountsService.class => AccountsServiceImpl.class,
                IOpportunitiesService.class => OpportunitiesServiceImpl.class });

    // SObjectType -> selector class.
    public static final fflib_Application.SelectorFactory Selector =
        new fflib_Application.SelectorFactory(
            new Map<SObjectType, Type> {
                Account.SObjectType => AccountsSelector.class,
                Opportunity.SObjectType => OpportunitiesSelector.class });

    // SObjectType -> domain constructor class.
    public static final fflib_Application.DomainFactory Domain =
        new fflib_Application.DomainFactory(
            Application.Selector,
            new Map<SObjectType, Type> {
                Account.SObjectType => Accounts.Constructor.class,
                Opportunity.SObjectType => Opportunities.Constructor.class });
}

newInstance resolves the registered Type and calls Type.newInstance(), so every registered class needs a public zero-argument constructor. Unregistered lookups throw fflib_Application.DeveloperException with No implementation registered for service interface X or Selector class not found for SObjectType X (fflib_Application.cls). Full binding tables, the UserModeDML factory subclass and the common wiring mistakes are in references/application-factory.md.

3. Naming and file layout

The samplecode conventions, verified against its class paths:

LayerInterfaceImplementationCaller writes
ServiceIAccountsServiceAccountsServiceImplAccountsService.updateOpportunityActivity(ids) (static facade)
DomainIAccountsAccounts (+ inner Constructor)(IAccounts) Application.Domain.newInstance(ids)
SelectorIAccountsSelector extends fflib_ISObjectSelectorAccountsSelectorAccountsSelector.newInstance()
Unit of Workfflib_ISObjectUnitOfWorkfflib_SObjectUnitOfWorkApplication.UnitOfWork.newInstance()

The service layer is three files: a with sharing static facade (AccountsService), the interface (IAccountsService), and the implementation (AccountsServiceImpl). The facade exists so callers keep static call syntax while the factory still controls instantiation:

public with sharing class AccountsService {
    public static void updateOpportunityActivity(Set<Id> accountIds) {
        service().updateOpportunityActivity(accountIds);
    }
    private static IAccountsService service() {
        return (IAccountsService) Application.Service.newInstance(IAccountsService.class);
    }
}

Directory layout inside a package directory (samplecode uses exactly these folder names):

force-app/main/default/classes/
  Application.cls
  service/      AccountsService.cls  IAccountsService.cls  AccountsServiceImpl.cls
  domains/      Accounts.cls         IAccounts.cls
  selectors/    AccountsSelector.cls IAccountsSelector.cls
  triggerHandlers/ AccountsTriggerHandler.cls
  controllers/  batchjobs/  restapis/

4. Installation: vendor, submodule, or deploy

fflib-apex-common @ master publishes no unlocked-package version id and no GitHub release; the repo has zero releases and its README advertises only a GitHub-deploy button. Any 04t... install id for these libraries is [unverified] - do not put one in a script.

# Option A (recommended): vendor into a dedicated package directory, pinned to a commit.
git clone https://github.com/apex-enterprise-patterns/fflib-apex-mocks.git /tmp/fflib-mocks
git -C /tmp/fflib-mocks checkout d81e9e1
mkdir -p fflib/main/default
cp -R /tmp/fflib-mocks/sfdx-source/apex-mocks/main/classes fflib/main/default/classes
# then fflib-apex-common on top of the same directory

# Deploy order matters only if your own tests reference ApexMocks: mocks first, then common.
sf project deploy start --source-dir fflib --target-org vf-dev --wait 20

fflib-apex-common's current README states "Dependencies: None for deployment or for running this library's Apex tests"; its docs/changelog.md still carries the September 2014 note that ApexMocks "must be deployed to the org before deploying this library". The README reflects master - deploy mocks first anyway, it is free and removes the ambiguity. Submodule and upgrade procedures: references/install-and-layout.md. Packaging consequences: skills sf-project-structure and sf-packaging-release.

5. Mock injection points

setMock on every factory is protected and @TestVisible, so it compiles only from test context (fflib_Application.cls).

CallArgument type
Application.UnitOfWork.setMock(uowMock)fflib_ISObjectUnitOfWork
Application.Service.setMock(IAccountsService.class, serviceMock)Type, Object
Application.Selector.setMock(selectorMock)fflib_ISObjectSelector - requires sObjectType() stubbed
Application.Selector.setMock(Account.SObjectType, selectorMock)SObjectType, fflib_ISObjectSelector
Application.Domain.setMock(domainMock)fflib_ISObjectDomain (uses sObjectType()) or fflib_IDomain (uses getType())
Application.Domain.setMock(Account.SObjectType, domainMock)SObjectType, fflib_ISObjectDomain

The single-argument selector and domain overloads call sObjectType() on the mock; if it is not stubbed the mock returns null and registration silently binds to null. fflib logs an INFO System.debug in that path. Prefer the two-argument overload. Detail: skill sf-fflib-testing.

6. Incremental migration from legacy Apex (strangler)

StepActionOld code
1Deploy fflib-apex-mocks + fflib-apex-common, add empty Application.clsuntouched
2Extract queries of the busiest SObject into XSelector, register itlegacy class calls XSelector.newInstance().selectById(ids)
3Introduce fflib_ISObjectUnitOfWork in the one method with the most DML statementsDML removed from legacy class
4Create IXService/XServiceImpl, move the legacy method body in verbatimlegacy public method becomes a one-line delegator
5Introduce the Domain class only once two callers need the same record-level rule-
6Convert the trigger last, after Service and Selector exist-

Keep the legacy class as a thin delegator until every caller is migrated, then delete it in the same release; do not leave two ways to do the same thing. Never mix a legacy [SELECT ...] and a selector for the same SObject in one class.

7. Static analysis and performance characteristics

fflib's own source suppresses PMD rules (fflib_SObjectSelector.cls header suppresses PMD.CyclomaticComplexity, PMD.ExcessivePublicCount, PMD.ApexDoc and more), so the library deploys clean only if your analyzer run excludes vendored paths. In your own fflib code expect:

RuleTriggerHandling
ApexCRUDViolationDatabase.query(qf.toSOQL()) - PMD cannot see the WITH USER_MODE that toSOQL() emitsconfigure readAuthMethodPattern for your selector base, or scope the rule out of selectors/
ApexSOQLInjectiondynamic query string built by fflib_QueryFactorysame; use Database.queryWithBinds for user input
ApexSharingViolationsAccountsServiceImpl without a sharing keywordalways declare with sharing or inherited sharing
ApexDoc, ApexUnitTestClassShouldHaveAssertsgenerated interfaces, Impl classeskeep as advisory, not gating

Exclude the vendored fflib/ directory from the Code Analyzer target; see skill sf-code-analyzer-quality. Governor cost of the factories: each Type.newInstance() is a reflective instantiation (no limit consumed, small CPU cost); fflib_SObjectDescribe caches describes per transaction, so a selector's first getSObjectFieldList() pays describe cost once. The real risks are getSObjectFieldList() bloat (heap) and per-record selector calls (SOQL 100 limit) - skills sf-governor-limits and sf-soql-sosl-optimization.

Anti-patterns

new AccountsServiceImpl() in product code. Bypasses Application.Service, so setMock cannot intervene and every consumer test needs DML.

// Wrong
IAccountsService svc = new AccountsServiceImpl();
// Right
IAccountsService svc = (IAccountsService) Application.Service.newInstance(IAccountsService.class);

Unit of Work SObjectType list in the wrong order. fflib_SObjectUnitOfWork commits in the order given; children before parents produces REQUIRED_FIELD_MISSING on the lookup at commit time. List Account before Opportunity before OpportunityLineItem.

One Application per feature. Two factories means two registries and two mock registries; setMock on one is invisible to the other. One Application class per package directory.

Service method taking a single record. updateOne(Id accountId) guarantees a governor failure when a trigger calls it per record. Take Set<Id> / List<SObject>.

Domain class issuing SOQL. Makes the domain untestable without DML and hides query cost inside record loops. Query in the service, pass records down.

Registering the domain class instead of its constructor. Account.SObjectType => Accounts.class fails at runtime unless Accounts itself implements a constructor interface; the samplecode registers Accounts.Constructor.class. See references/application-factory.md.

Verification

# 1. Local gate: format, lint, analyzer, jest. No org needed.
node "${CLAUDE_PLUGIN_ROOT}/scripts/checks/vf-check.mjs" local --changed

# 2. Compile + run the fflib-facing tests in an org.
sf project deploy start --source-dir force-app --target-org vf-dev --wait 30
sf apex run test --target-org vf-dev --synchronous \
  --tests AccountsServiceTest --tests AccountsSelectorTest \
  --code-coverage --result-format human

# 3. Prove the factories resolve every registration (fails fast on a missing binding).
sf apex run --target-org vf-dev --file scripts/apex/assert-application-wiring.apex

# 4. Coverage + analyzer gates (apexOrgCoverageMin 85, apexClassCoverageMin 75, severity 3).
node "${CLAUDE_PLUGIN_ROOT}/scripts/checks/vf-check.mjs" apex --target-org vf-dev
node "${CLAUDE_PLUGIN_ROOT}/scripts/checks/vf-check.mjs" analyzer

scripts/apex/assert-application-wiring.apex (anonymous Apex, run after every Application.cls edit):

for (SObjectType t : new List<SObjectType>{ Account.SObjectType, Opportunity.SObjectType }) {
    fflib_ISObjectSelector s = Application.Selector.newInstance(t);
    System.assertEquals(t, s.sObjectType(), 'Selector bound to wrong SObjectType: ' + t);
}
System.assert(Application.Service.newInstance(IAccountsService.class) instanceof IAccountsService);
System.assertNotEquals(null, Application.UnitOfWork.newInstance());

References

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.