As Salesforce implementations grow, unstructured Apex quickly turns into spaghetti code. When multiple developers write SOQL queries and DML statements randomly across triggers, batch classes, and LWC controllers, the system becomes fragile and unscalable. To solve this, developers use the Enterprise Design Patterns (often modeled via the fflib architecture) to give every piece of logic a strict, predictable home.
Key Points: The Four Layers of Apex
The core concept is to assign exactly one job to each layer of your code.
- Selector Layer: The absolute only place where SOQL queries live.
- Domain Layer: Holds per-object behaviors like validation rules and defaulting logic. It executes whenever records of that specific type are touched, usually wrapping the trigger execution contexts.
- Service Layer: Holds cross-object business processes and specific use-cases. It acts as the central API that other code (like Controllers, Batch jobs, or REST endpoints) calls.
- Unit of Work: Collects all DML operations in memory and commits them to the database in one clean, ordered step at the very end of the transaction.
Real-Life Scenario: The Query That Lived in Nine Places
Imagine you have a SOQL query that fetches "Active Deliveries for an Account." Over time, different developers have written this exact query in nine different classes.
- SELECTOR: Create a
DeliverySelectorclass. This is the only home for that SOQL query. - DOMAIN: The delivery validation rules live in a single Domain class.
- SERVICE: Cross-object logic (like converting a delivery to a billing invoice) lives in
BillingService. - UNIT OF WORK: Any resulting record updates are passed to the Unit of Work to be saved safely.
DeliverySelector, and all nine call sites automatically get the new data for free.
Frequently Asked Questions
Selector prevents query drift and inconsistent FLS enforcement. Domain prevents scattered object rules and ensures validation happens even during Batch runs. Service prevents business logic from being trapped and duplicated inside UI controllers. Unit of Work prevents partial database commits and DML order-of-execution bugs (like trying to insert child records before the parent exists).
Yes, it can be overkill. The cost is extra complexity. You will have to build interfaces, factories, and maintain a high class count. For a small two-admin org, this will slow development to a crawl and deeply confuse junior developers. However, when the pain of multiple teams stepping on each other's code arrives, the architecture becomes necessary to scale safely.
They become layer-wide policies rather than per-method guesses.
- Selectors should default to
WITH USER_MODEso the query only returns what the user is allowed to see. - Services should declare
inherited sharingso they respect the security context of whatever called them (like an LWC controller). - Entry points (like
@AuraEnabledcontrollers) should explicitly declarewith sharing.
- Rule: Four layers, one job each. Stop writing logic in triggers.
- Gain: Fixes query drift, scattered logic, and DML limits. Makes unit testing exponentially faster by allowing mock interfaces.
- Price: High upfront overhead and complexity.
fflibis community-owned, so your team owns the upgrade and maintenance path. - When to adopt: Buy into this architecture when the pain of managing code at scale arrives, not on day one of a fresh, small org.