Skip to main content

The Salesforce Separation of Concerns: Understanding Service, Domain, and Selector Layers

In plain words: Large enterprise organizations split their Apex code into specific functional "floors" or layers. The Service layer holds the business logic, the Domain layer handles object-specific rules, the Selector layer manages all queries, and the Unit of Work executes database commits safely. This is known as "Separation of Concerns."

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.

The Old/Bad Way: Every trigger and controller writes its own SOQL. If the business requests a new field to be returned, you have to update the query in nine places. If you miss one, things break. Furthermore, FLS (Field-Level Security) is likely enforced inconsistently across those nine queries.
The Layered Solution:
  • SELECTOR: Create a DeliverySelector class. 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.
The Payoff: Now, when a new field is required, you add it to one method in the DeliverySelector, and all nine call sites automatically get the new data for free.

Frequently Asked Questions

Q: What specific failures does each layer prevent?

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).

Q: What is the honest cost of this architecture? Is it overkill?

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.

Q: Where do Sharing and Field-Level Security (FLS) decisions live?

They become layer-wide policies rather than per-method guesses.

  • Selectors should default to WITH USER_MODE so the query only returns what the user is allowed to see.
  • Services should declare inherited sharing so they respect the security context of whatever called them (like an LWC controller).
  • Entry points (like @AuraEnabled controllers) should explicitly declare with sharing.
Diagram showing the flow of Service, Selector, and Domain Layers in Salesforce architecture
360 Card: The Separation of Concerns
  • 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. fflib is 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.
Core Takeaway: A trigger handler is just an entry point—it does not tell you where the logic should actually live. By separating your code into Service, Domain, Selector, and Unit of Work layers, you create a robust, testable enterprise system where code is reused rather than duplicated.