Domain Layer Pattern with Fluent Interface

Overview

circle-info

When to use this pattern: When building business logic that operates on collections of SObjects, requires filtering/transformation operations, and needs to maintain readability while ensuring testability and reusability.

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

Component
Purpose
Responsibility

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

  1. Framework Dependency: Install fflib-apex-common and fflib-apex-extensions

  2. Application 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* or selectWith* prefix

  • Name mutation methods with set* prefix

  • Return this from mutation methods to enable chaining

  • Return new domain instances from selection methods (immutable filtering)

  • Use Schema tokens (Schema.Account.Field__c) for compile-time safety

  • Provide 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 sharing on domain classes to respect record-level security

  • Field-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

  1. Selection Filtering: Each selectBy* method correctly filters records

  2. Method Chaining: Chained operations work correctly together

  3. Mutation: set* methods correctly update field values

  4. Empty Collections: Methods handle empty collections gracefully

  5. Accessor Methods: Data extraction methods return correct values

Trade-offs

Benefit
Cost

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?