Domain Layer Pattern with Fluent Interface
Overview
Purpose
The Domain Layer with Fluent Interface pattern encapsulates business logic for SObject types in dedicated classes that support method chaining. This enables expressive, readable code that clearly communicates intent while providing strong type safety and comprehensive testability.
Context
Enterprise Salesforce implementations often suffer from:
Business logic scattered across triggers, services, and controllers
Repeated filtering and transformation code
Difficult-to-test code due to tight coupling with SObjects
Poor readability due to verbose iteration patterns
The Domain Layer consolidates all SObject-specific logic into cohesive, chainable interfaces.
Problem Statement
The Challenge
Working directly with List<SObject> leads to several issues:
// Anti-pattern: Verbose, repetitive, hard to maintain
List<Account> activeAccounts = new List<Account>();
for (Account acc : accounts) {
if (acc.Account_Status__c == 'Active') {
activeAccounts.add(acc);
}
}
List<Account> activeUsAccounts = new List<Account>();
for (Account acc : activeAccounts) {
if (acc.BillingCountryCode == 'US') {
activeUsAccounts.add(acc);
}
}
for (Account acc : activeUsAccounts) {
acc.Invoice_Batch__c = 'Batch 1';
}
This pattern repeats throughout the codebase, is error-prone, and obscures business intent.
Why Traditional Approaches Fall Short
Utility Classes: Static methods don't support chaining and feel disconnected from the data
Extension Methods: Apex doesn't support them
Direct List Manipulation: No encapsulation, duplicated filtering logic, poor testability
Solution
Core Concept
Wrap List<SObject> in domain classes that expose business operations as chainable methods. Selection methods (select*) return filtered domain instances, mutation methods (set*) modify records and return the same instance for chaining and accessor methods (get*) retrieve data from the records (primitive data types).
Implementation Strategy
1. Define the Domain Interface
The interface declares all operations available on the domain:
2. Implement the Domain Class
The domain class extends the framework base and implements the interface:
Key Components
IAccounts
Interface
Defines the public API for the domain
Accounts
Implementation
Contains all business logic for Account records
fflib_SObjects2
Base Class
Provides reusable selection and accessor methods
Application.domain
Factory
Creates domain instances, supports mocking
Constructor
Inner Class
Enables factory-based instantiation
Implementation Details
Required Setup
Framework Dependency: Install
fflib-apex-commonandfflib-apex-extensionsApplication Class: Configure domain factory:
Code Structure - Using the Domain in Trigger Handler
Configuration Requirements
Custom Settings/Metadata: None required for the pattern itself
Permissions: Standard object permissions apply
Dependencies:
fflib-apex-common,fflib-apex-extensions
Best Practices
Do's
Name selection methods with
selectBy*orselectWith*prefixName mutation methods with
set*prefixReturn
thisfrom mutation methods to enable chainingReturn new domain instances from selection methods (immutable filtering)
Use Schema tokens (
Schema.Account.Field__c) for compile-time safetyProvide both single-value and set-based overloads for flexibility
Include
isEmpty()checks to short-circuit empty collections
Don'ts
Do not perform DML inside domain methods (that belongs in Services or UoW)
Do not query data inside domain methods (domains work on in-memory records)
Do not throw exceptions from selection methods (return empty domain instead)
Do not mix selection and mutation in a single method
Considerations
Governor Limits
Domain operations are in-memory, no SOQL/DML overhead
Chained operations iterate the collection once per method (be mindful with large datasets)
Early
isEmpty()returns prevent unnecessary iterations
Performance Impact
In-memory filtering is extremely fast
Each selection creates a new List, but this is negligible for typical record volumes
For very large datasets (10k+ records), consider batch processing instead
Security Implications
Use
with sharingon domain classes to respect record-level securityField-level security is not automatically enforced; consider using
Security.stripInaccessible()
Variations
Variation 1: Exclusion Methods
Add selectByFieldNotIn methods for inverse filtering:
Variation 2: Conditional Mutation
Combine selection with map-based value assignment:
Variation 3: Cross-Domain Data Extraction
Extract data for use with related domains:
Testing Approach
Unit Test Strategy
Test Scenarios
Selection Filtering: Each
selectBy*method correctly filters recordsMethod Chaining: Chained operations work correctly together
Mutation:
set*methods correctly update field valuesEmpty Collections: Methods handle empty collections gracefully
Accessor Methods: Data extraction methods return correct values
Trade-offs
Highly readable, expressive code
Learning curve for team members
Excellent testability (no mocking needed)
Additional classes to maintain
Strong type safety via interfaces
Memory overhead from creating new Lists
Reusable across triggers, services, batch
Framework dependency
Self-documenting business logic
Upfront implementation effort
Last updated
Was this helpful?