Skills DirectorySkills Directory
SkillsLearnSecurityCategoriesDocsCommunityBlog
Sign InSubmit Skill
Skills Directory

Security-tested agent skills for Claude, coding agents, and AI workflows.

Directory

  • Browse Skills
  • All Skills A–Z
  • Claude Skills
  • Claude Code Skills
  • Agent Skills
  • Categories
  • Submit a Skill

Learn

  • Learn Hub
  • Install Claude Skills
  • Write SKILL.md
  • Skills vs MCP
  • Directories Compared

Security

  • Security
  • Methodology
  • Secure Claude Skills
  • Security Badges

Company

  • About
  • Community
  • Blog
  • API Docs
  • Advertise

2026 Skills Directory. All rights reserved.

Back to skills

Sf Fflib Domain Service Uow

ASecurity

Covers the write side of the fflib Apex Enterprise Patterns stack - fflib_SObjectDomain trigger domains (onApplyDefaults, onValidate, onBeforeInsert, onAfterUpdate, handleBeforeInsert dispatch, Configuration flags, DomainException), the service layer (IXxxService + XxxServiceImpl + Application.Service.newInstance, transaction ownership, thin @AuraEnabled and @InvocableMethod entry points), and fflib_SObjectUnitOfWork (registerNew, registerDirty, registerRelationship, registerPublishAfterSucce...

2 stars
0 votes
0 copies
0 views
Added 9/19/2026
developmentgoshellbashnodetestinggitapidatabasesecurity

Works with

api

Security Analysis

A100/100

Scanned 9/19/2026

Install to Claude Code

$npx -y skills add grzmol/vibe-force --skill sf-fflib-domain-service-uow --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Sf Fflib Domain Service Uow?

Add the live security badge to your README — it updates automatically with every re-scan.

Security grade badge for Sf Fflib Domain Service Uow
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/grzmol-sf-fflib-domain-service-uow/badge)](https://www.skillsdirectory.com/skills/grzmol-sf-fflib-domain-service-uow)

More formats (shields.io, HTML) on the badges page.

Download Zip
Files
SKILL.md
---
name: sf-fflib-domain-service-uow
description: Covers the write side of the fflib Apex Enterprise Patterns stack - fflib_SObjectDomain trigger domains (onApplyDefaults, onValidate, onBeforeInsert, onAfterUpdate, handleBeforeInsert dispatch, Configuration flags, DomainException), the service layer (IXxxService + XxxServiceImpl + Application.Service.newInstance, transaction ownership, thin @AuraEnabled and @InvocableMethod entry points), and fflib_SObjectUnitOfWork (registerNew, registerDirty, registerRelationship, registerPublishAfterSuccessTransaction, commitWork, custom IDML with AccessLevel.USER_MODE, SObject commit ordering). Use this skill when writing or reviewing a one-line fflib trigger, a domain class with a Constructor inner class, a service that owns commitWork, a Unit of Work registration chain, or when deciding where a piece of business logic belongs in an fflib org.
---

# fflib Domain, Service and Unit of Work

The write side of Apex Enterprise Patterns. Selectors read (skill `sf-fflib-selector-layer`);
Domain, Service and Unit of Work decide, validate and write.

All `fflib_*` signatures below were read from `apex-enterprise-patterns/fflib-apex-common`,
branch `master`, commit `dab59777bac37f6b8a3cc2871fcf5df8d37764b4` (2026-09-10), under
`sfdx-source/apex-common/main/classes/`. Wiring examples were read from
`apex-enterprise-patterns/fflib-apex-common-samplecode`, branch `master`, commit
`4657d6325b3d6ae41bc7af864ca4f41287e72112`.

## When to use

| Situation | Use this skill |
| --- | --- |
| Writing a trigger in an fflib org | Yes - one-line `fflib_SObjectDomain.triggerHandler` body |
| Record-level validation that must fire on every write path | Yes - domain `onValidate` |
| Orchestrating multi-object writes in one transaction | Yes - service + Unit of Work |
| Exposing logic to LWC, Flow, REST or Batch | Yes - thin entry point delegating to a service |
| Querying with field-level security and a query factory | No - skill `sf-fflib-selector-layer` |
| `Application` factory wiring, `fflib_ISObjectUnitOfWork` mocking basics | No - skill `sf-fflib-foundations` |
| Writing the tests for any of the above | No - skill `sf-fflib-testing` |
| Bypass switches, async runners, packaging, upgrades | No - skill `sf-fflib-operations` |
| Hand-rolled trigger handler frameworks, no fflib | No - skill `sf-apex-development` |

## Decision table: where does the logic go?

| Logic | Layer | Why |
| --- | --- | --- |
| Default a field on insert | Domain `onApplyDefaults()` | Runs before insert, no DML needed |
| "Field A is required when B is set" | Domain `onValidate()` | Must hold for every write path including Data Loader |
| "Field X cannot change after creation" | Domain `onValidate(Map<Id,SObject> existingRecords)` | Needs old values |
| Compute a roll-up onto a parent | Domain method invoked by a service, writes via UoW | Needs cross-object transaction |
| "Close the opportunity and create the project" | Service method | Owns the transaction, spans objects |
| Query the records first | Selector, called by the service | Domain classes never query |
| Send the platform event | UoW `registerPublishAfterSuccessTransaction` | Publish only if the transaction commits |
| Enqueue async follow-up | Service method returning the job Id | Keeps `System.enqueueJob` out of domain code |
| Convert records to a UI shape | Controller/DTO, not the domain | Domain holds behaviour, not presentation |

## Core patterns

### 1. The one-line trigger

The trigger contains a single dispatch call. The dispatcher resolves the domain constructor,
constructs the domain over the correct record set and calls the matching `handleXxx` method.

```apex
trigger Opportunities on Opportunity (
    before insert, before update, before delete,
    after insert, after update, after delete, after undelete)
{
    fflib_SObjectDomain.triggerHandler(OpportunitiesTriggerHandler.class);
}
```

`triggerHandler(Type domainClass)` resolves the constructor as
`domainClassName.endsWith('Constructor') ? Type.forName(domainClassName) : Type.forName(domainClassName + '.Constructor')`,
so every domain class registered for trigger dispatch **must** expose a public inner class named
`Constructor` implementing `fflib_SObjectDomain.IConstructable` (or `IConstructable2` when the
SObjectType must be passed for empty lists). Compare with the hand-rolled dispatcher in skill
`sf-apex-development`: fflib replaces the `if (Trigger.isBefore && Trigger.isInsert)` ladder and the
handler interface with this single static call.

### 2. Domain class anatomy

```apex
public inherited sharing class OpportunitiesTriggerHandler extends fflib_SObjectDomain
{
    public OpportunitiesTriggerHandler(List<Opportunity> sObjectList)
    {
        super(sObjectList);
        // Opt out of the base-class CRUD check when a custom IDML enforces user mode instead
        Configuration.disableTriggerCRUDSecurity();
    }

    public override void onApplyDefaults()
    {
        for (Opportunity opp : (List<Opportunity>) Records)
        {
            if (opp.CloseDate == null) { opp.CloseDate = System.today().addDays(30); }
        }
    }

    public override void onValidate()
    {
        for (Opportunity opp : (List<Opportunity>) Records)
        {
            if (opp.Type != null && opp.Type.startsWith('Existing') && opp.AccountId == null)
            {
                opp.AccountId.addError(
                    error('Existing-customer Opportunities require an Account.', opp, Opportunity.AccountId));
            }
        }
    }

    public override void onValidate(Map<Id, SObject> existingRecords)
    {
        for (Opportunity opp : (List<Opportunity>) Records)
        {
            Opportunity existing = (Opportunity) existingRecords.get(opp.Id);
            if (opp.Type != existing.Type)
            {
                opp.Type.addError(error('Opportunity Type is immutable.', opp, Opportunity.Type));
            }
        }
    }

    public class Constructor implements fflib_SObjectDomain.IConstructable
    {
        public fflib_SObjectDomain construct(List<SObject> sObjectList)
        {
            return new OpportunitiesTriggerHandler(sObjectList);
        }
    }
}
```

`Records` is a property returning `getRecords()` (inherited from `fflib_SObjects`); the domain owns
its own copy of the list. `error(...)` records the message in `fflib_SObjectDomain.Errors` so tests
can assert on validation without DML. Full dispatch table and every `Configuration` flag:
[references/domain-layer.md](references/domain-layer.md).

### 3. Service owns the transaction

```apex
public interface IProjectsService
{
    Set<Id> createFromOpportunities(Set<Id> opportunityIds);
}
```

```apex
public with sharing class ProjectsService
{
    public static Set<Id> createFromOpportunities(Set<Id> opportunityIds)
    {
        return service().createFromOpportunities(opportunityIds);
    }

    private static IProjectsService service()
    {
        return (IProjectsService) Application.Service.newInstance(IProjectsService.class);
    }
}
```

The static shim keeps call sites terse; `Application.Service.newInstance` returns the registered
implementation or a mock injected by `setMock`. The implementation creates the Unit of Work, hands
it to domain methods, and is the only place that calls `commitWork()`. Never call `commitWork()`
inside a domain method or a trigger context - the caller owns the transaction.

### 4. Unit of Work registration chain

```apex
public with sharing class ProjectsServiceImpl implements IProjectsService
{
    public Set<Id> createFromOpportunities(Set<Id> opportunityIds)
    {
        fflib_ISObjectUnitOfWork uow = Application.UnitOfWork.newInstance();

        List<Opportunity> opps = OpportunitiesSelector.newInstance()
            .selectByIdWithLineItems(opportunityIds);

        List<Project__c> created = new List<Project__c>();
        for (Opportunity opp : opps)
        {
            Project__c project = new Project__c(
                Name = opp.Name.left(80),
                Account__c = opp.AccountId,
                Stage__c = 'Planning');
            uow.registerNew(project, Project__c.Opportunity__c, opp);  // opp already has an Id
            created.add(project);

            Integer sequence = 1;
            for (OpportunityLineItem line : opp.OpportunityLineItems)
            {
                ProjectTask__c task = new ProjectTask__c(
                    Name = line.Description == null ? 'Task ' + sequence : line.Description.left(80),
                    Sequence__c = sequence++);
                uow.registerNew(task, ProjectTask__c.Project__c, project); // parent has no Id yet
            }

            uow.registerPublishAfterSuccessTransaction(
                new ProjectEvent__e(Opportunity__c = opp.Id, Action__c = 'Created'));
        }

        uow.commitWork();

        Set<Id> projectIds = new Set<Id>();
        for (Project__c project : created) { projectIds.add(project.Id); }
        return projectIds;
    }
}
```

`registerNew(SObject record, Schema.SObjectField relatedToParentField, SObject relatedToParentRecord)`
registers the child and defers the lookup assignment until the parent is inserted. The order of
`SObjectType`s passed to `Application.UnitOfWork` decides insert order, so `Project__c` must appear
before `ProjectTask__c`. Full API table, relationship variants and commit lifecycle:
[references/unit-of-work.md](references/unit-of-work.md).

### 5. Thin entry points

Every external caller is a shell over a service call. Controllers, invocable actions, REST resources
and async runners add no business logic.

```apex
public with sharing class ProjectCreationController
{
    @AuraEnabled
    public static List<Id> createProjects(List<Id> opportunityIds)
    {
        try
        {
            return new List<Id>(ProjectsService.createFromOpportunities(new Set<Id>(opportunityIds)));
        }
        catch (Exception e)
        {
            throw new AuraHandledException(e.getMessage());
        }
    }
}
```

```apex
public with sharing class CreateProjectsAction
{
    public class Request
    {
        @InvocableVariable(required=true) public Id opportunityId;
    }

    @InvocableMethod(label='Create Project From Opportunity' category='Projects')
    public static void run(List<Request> requests)
    {
        Set<Id> ids = new Set<Id>();
        for (Request r : requests) { ids.add(r.opportunityId); }
        ProjectsService.createFromOpportunities(ids);   // one bulk call, not one per request
    }
}
```

LWC wiring is in skill `sf-lwc-development`; Flow invocable semantics and bulk behaviour in skill
`sf-flow-automation`; Queueable and Batch entry points in skills `sf-async-apex-patterns` and
`sf-fflib-operations`.

### 6. User-mode DML through a custom `IDML`

`fflib_SObjectUnitOfWork.SimpleDML` performs `System.Database.insert(objList, AccessLevel.SYSTEM_MODE)`.
`fflib_SObjectUnitOfWork.UserModeDML extends SimpleDML` and defaults to `AccessLevel.USER_MODE`. Wire
it once in the `Application` factory rather than at every call site:

```apex
private class UserModeUnitOfWorkFactory extends fflib_Application.UnitOfWorkFactory
{
    public UserModeUnitOfWorkFactory(List<SObjectType> objectTypes) { super(objectTypes); }

    public override fflib_ISObjectUnitOfWork newInstance()
    {
        if (m_mockUow != null) { return m_mockUow; }
        return new fflib_SObjectUnitOfWork(m_objectTypes, new fflib_SObjectUnitOfWork.UserModeDML());
    }
}
```

This is exactly the pattern used by `Application.cls` in fflib-apex-common-samplecode. Permission
semantics and `AccessLevel` trade-offs: skill `sf-security-model`.

## Anti-patterns

**Committing inside the domain.**

```apex
// WRONG - the domain does not own the transaction and cannot be composed
public override void onAfterInsert()
{
    fflib_ISObjectUnitOfWork uow = Application.UnitOfWork.newInstance();
    uow.registerNew(buildAuditRecords());
    uow.commitWork();
}
```

```apex
// RIGHT - domain methods accept the caller's UoW
public void logAudit(fflib_ISObjectUnitOfWork uow)
{
    uow.registerNew(buildAuditRecords());
}
```

**Row-at-a-time service signatures.**

```apex
// WRONG - callers loop, every loop iteration opens a transaction
void createProject(Id opportunityId);
```

```apex
// RIGHT - bulk-first, the service loops internally once
Set<Id> createFromOpportunities(Set<Id> opportunityIds);
```

**SOQL inside a domain class.** Domain classes receive records; they never query. Inject data via a
selector call in the service, or pass a pre-queried list. See skill `sf-fflib-selector-layer`.

**Hand-rolled savepoints around a `commitWork()`.** `commitWork()` already sets a savepoint, rolls
back on exception and rethrows. Wrapping it adds two more DML statements against the 150-statement
limit ([Apex transaction control](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/langCon_apex_transaction_control.htm)).
Only add an outer savepoint when you must keep earlier, already-committed work in the same
transaction.

**`registerDirty` on a record the domain also mutates after registration.** The UoW holds the same
reference; late mutations silently ship. Register last, or use the field-scoped
`registerDirty(SObject, List<SObjectField>)` overload.

**Multiple `commitWork()` calls to "flush" work.** Each call is a fresh savepoint plus DML per
SObject type. Register everything, commit once. Governor arithmetic: skill `sf-governor-limits`.

**A second trigger on the same object.** fflib assumes one trigger per object; a second trigger
re-enters the dispatcher with a different record set and breaks stateful domains. See skill
`sf-fflib-operations` for the inventory and migration procedure.

## Verification

```bash
# Local gate: format, lint, Code Analyzer, Jest
node "${CLAUDE_PLUGIN_ROOT}/scripts/checks/vf-check.mjs" local --changed

# Static analysis only (PMD rules that misfire on fflib are listed in sf-fflib-operations)
node "${CLAUDE_PLUGIN_ROOT}/scripts/checks/vf-check.mjs" analyzer --changed

# Org tests plus coverage gates (apexOrgCoverageMin 85, apexClassCoverageMin 75)
node "${CLAUDE_PLUGIN_ROOT}/scripts/checks/vf-check.mjs" apex --target-org vf-dev

# Narrow run while iterating on one domain or service
sf apex run test --target-org vf-dev --synchronous --code-coverage --result-format human \
  --tests ProjectsServiceTest --tests OpportunitiesTriggerHandlerTest

# Prove the trigger dispatches and the UoW commits in a real org
sf apex run --target-org vf-dev --file scripts/apex/create-projects-smoke.apex
sf data query --target-org vf-dev \
  --query "SELECT Id, Name, Opportunity__c FROM Project__c ORDER BY CreatedDate DESC LIMIT 5"
```

Checklist before opening the PR:

| Check | Expected |
| --- | --- |
| Trigger body | Exactly one `fflib_SObjectDomain.triggerHandler(...)` call |
| Domain class | Has `Constructor` inner class implementing `IConstructable` |
| Domain class | No SOQL, no DML, no `commitWork()` |
| Service interface | Bulk signatures (`Set<Id>`, `List<...>`, `Map<Id,...>`) |
| Service impl | Creates UoW, calls `commitWork()` exactly once per public method |
| UoW type list | Parents before children in `Application.UnitOfWork` |
| Platform events | Registered with `registerPublishAfterSuccessTransaction` unless failure telemetry is wanted |
| Entry points | `@AuraEnabled` / `@InvocableMethod` contain no business logic |

## References

- [references/domain-layer.md](references/domain-layer.md) - dispatch table, every override, `Configuration`
  flags, `TriggerEvent` toggles, stateful domains, `Test.Database` mock DML, error handling.
- [references/service-layer.md](references/service-layer.md) - interface/impl/controller/invocable chain for
  one feature, exception policy, transaction ownership, DTO rules.
- [references/unit-of-work.md](references/unit-of-work.md) - full `fflib_ISObjectUnitOfWork` API table,
  relationship registration, platform events, custom `IDML`, commit order, limits.
- [references/end-to-end-feature.md](references/end-to-end-feature.md) - one requirement implemented across
  Selector, Domain, Service, UoW, controller and invocable action, with every file.

Sibling skills: `sf-fflib-foundations`, `sf-fflib-selector-layer`, `sf-fflib-testing`,
`sf-fflib-operations`, `sf-apex-development`, `sf-apex-testing`, `sf-async-apex-patterns`,
`sf-governor-limits`, `sf-security-model`, `sf-soql-sosl-optimization`, `sf-lwc-development`,
`sf-flow-automation`, `sf-integration-patterns`, `sf-code-analyzer-quality`.

Upstream sources:

- [fflib-apex-common](https://github.com/apex-enterprise-patterns/fflib-apex-common) - `master`,
  `sfdx-source/apex-common/main/classes/fflib_SObjectDomain.cls`, `fflib_SObjectUnitOfWork.cls`,
  `fflib_ISObjectUnitOfWork.cls`, `fflib_Application.cls`, `fflib_SObjects.cls`.
- [fflib-apex-common-samplecode](https://github.com/apex-enterprise-patterns/fflib-apex-common-samplecode) -
  `Application.cls`, `domains/Opportunities.cls`, `service/OpportunitiesServiceImpl.cls`,
  `triggerHandlers/OpportunitiesTriggerHandler.cls`, `triggers/Opportunities.trigger`.
- [fflib.dev](https://fflib.dev/docs/domain-layer/example) - layer conventions.
- [Apex transaction control](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/langCon_apex_transaction_control.htm),
  [Execution governors and limits](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_gov_limits.htm),
  [Publish platform events with Apex](https://developer.salesforce.com/docs/atlas.en-us.platform_events.meta/platform_events/platform_events_publish_apex.htm).

Attribution

grzmolgrzmol
View sourceMore from grzmol →
SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments (0)

No comments yet. Be the first to comment!

SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Related Skills

Browser Extension Developer

Use this skill when developing or maintaining browser extension code in the `browser/` directory, including Chrome/Firefox/Edge compatibility, content scripts, background scripts, or i18n updates.

281612 votes

Seo Optimizer

SEO optimization with keyword analysis, readability assessment, technical validation, content quality. Use for search rankings, blog posts, content audits, or encountering keyword density, readability scores, meta tags, schema markup errors.

2132 votes

Google Official Seo Guide

Official Google SEO guide covering search optimization, best practices, Search Console, crawling, indexing, and improving website search visibility based on official Google documentation

1862 votes

Tanstack Start

Build a full-stack TanStack Start app on Cloudflare Workers from scratch — SSR, file-based routing, server functions, D1+Drizzle, better-auth, Tailwind v4+shadcn/ui. Use whenever the user mentions TanStack Start, asks to scaffold a full-stack Cloudflare app with SSR, wants an SSR dashboard, or asks for a React 19 + Cloudflare Workers app with file-based routing and server functions — even if they don't name TanStack Start specifically. No template repo — Claude generates every file fresh per ...

9881 votes

Pentest

PTES-aligned adversarial security audit for backend, frontend, and mobile applications. Produces a CVSS-scored Hacker Report with verified PoCs and phased remediation.

5491 votes
View all in development →