Start typing to search.

Salesforce Salesforce FFLib Enterprise Architecture

Salesforce Architecture with FFLib: Enterprise Patterns

Complete guide on implementing enterprise architectural patterns in Salesforce using FFLib. Includes Domain Layer, Selector Layer, Service Layer, and Unit of Work.

15 min read

Introduction: My Perspective on fflib

When it comes to developing enterprise applications on Salesforce, scalability and maintainability aren’t optional. I’ve seen many projects get tangled in their own complexity for lack of a clear structure. This is where fflib, an implementation of the Apex Enterprise Patterns, comes in. In my opinion, it’s one of the most powerful tools for a Salesforce developer.

More than just a library, fflib provides a common language and a set of blueprints for building software in an orderly fashion. It adopts a layered approach that enforces a Separation of Concerns, a fundamental principle often overlooked in Apex development. In this article, I want to share my practical experience with this architecture, breaking down its layers, the patterns it implements, and, most importantly, the real impact it has on development quality and agility.

The 4 Key Layers of the Architecture

The magic of fflib lies in its layered structure, where each layer has a very specific mission. Forget about monolithic classes and triggers that do everything. Here, the code is organized as follows:

  • Domain Layer: The guardian of record-level business logic. Think of it as the expert on a specific object (e.g., Account, Lead).
  • Selector Layer: Acts as a centralized repository for all SOQL queries for an object. It’s the only layer that should talk directly to the database to read data.
  • Service Layer: The brain that orchestrates complex operations. When a business process involves multiple objects or steps, the service takes control.
  • Unit of Work: Manages DML operations (insert, update, delete) intelligently, grouping them to ensure data integrity in a transaction.

Let’s analyze each one from a practical perspective.

Domain Layer: The First Line of Defense

In my experience, the Domain Layer is the first line of defense against data inconsistency and scattered business logic. Its responsibility is clear: to encapsulate all the rules, validations, and behaviors that belong to a single record of an object.

Imagine you have a rule that says, “A Lead cannot be contacted if their score is below 50.” Instead of coding this logic in a trigger, a Lightning controller, and a batch class, you centralize it in a method within LeadsDomain.

// LeadsDomain.cls
public class LeadsDomain extends fflib_SObjectDomain {
    // The constructor receives the list of records from the trigger
    public LeadsDomain(List<Lead> records) {
        super(records);
    }

    // Method invoked before an update
    public override void onBeforeUpdate(Map<Id, SObject> oldMap) {
        for(Lead newLead : (List<Lead>) this.records) {
            Lead oldLead = (Lead) oldMap.get(newLead.Id);
            // Centralized validation logic
            if (newLead.Status == 'Contacted' && newLead.Score__c < 50) {
                newLead.addError('A Lead with a score below 50 cannot be contacted.');
            }
        }
    }
}

My Opinion and Experience: Adopting the Domain Layer is a game-changer for triggers. I’ve been on projects where this pattern was key to encapsulating the object’s own DML operations and drastically simplifying the trigger lifecycle, ensuring consistent behavior. Although fflib’s trigger framework might seem complex at first, the value it provides by centralizing logic is immense and prevents the technical debt that arises from duplicated business rules.

Selector Layer: Smart, Reusable Queries

The Selector Layer is the materialization of the Repository pattern. Its sole mission is to build and execute SOQL queries. Instead of writing queries directly in your services or controllers, you request the data from the corresponding selector.

// ILeadsSelector.cls (Interface)
public interface ILeadsSelector extends fflib_ISObjectSelector {
    List<Lead> selectRecentLeadsWithHighValue(Integer limitSize);
}

// LeadsSelector.cls (Implementation)
public class LeadsSelector extends fflib_SObjectSelector implements ILeadsSelector {
    public List<Schema.SObjectField> getSObjectFieldList() {
        return new List<Schema.SObjectField>{
            Lead.Name, Lead.Company, Lead.Status, Lead.Score__c
        };
    }
    
    public List<Lead> selectRecentLeadsWithHighValue(Integer limitSize) {
        return (List<Lead>) Database.query(
            newQueryFactory()
                .setCondition('Score__c > 100 AND Status = \'New\'')
                .setOrderBy(new fflib_QueryFactory.OrderBy('CreatedDate', 'DESC'))
                .setLimit(limitSize)
                .toSOQL()
        );
    }
}

My Opinion and Experience: The most obvious benefit is reusability. But for me, the true power of Selectors lies in three aspects. First, consistency, which avoids the dreaded “SObject row was retrieved via SOQL without querying the requested field” exceptions. Second, optimization, as centralizing queries makes it easy to implement caching. And third, security. In a practical case I recall, the Selector pattern not only made queries more efficient but also allowed the team to easily create custom queries, reducing code duplication and minimizing the risk of SOQL injection.

Service Layer: The Brain of the Operation

If the Domain focuses on a single record, the Service is the orchestrator of complete business processes. It’s the layer that coordinates interactions between different domains and selectors to fulfill a use case.

For example, a service like LeadConversionService might:

  1. Call LeadsSelector to get the Lead’s data.
  2. Call AccountsSelector to see if an account already exists.
  3. Invoke AccountsDomain and ContactsDomain to prepare the new records.
  4. Use the UnitOfWork to register the creation of the Account, Contact, and the update of the Lead.
// ILeadConversionService.cls
public interface ILeadConversionService {
    void convertLeads(Set<Id> leadIds);
}

// LeadConversionService.cls
public class LeadConversionService implements ILeadConversionService {
    private fflib_ISObjectUnitOfWork uow;
    // ... Dependency injection for selectors and other services

    public void convertLeads(Set<Id> leadIds) {
        // 1. Orchestrate the logic...
        // 2. Call selectors...
        // 3. Use domains to apply rules...
        // 4. Register changes in the Unit of Work...
        uow.commitWork();
    }
}

My Opinion and Experience: The Service Layer is the glue that holds the architecture together. In a real application, I’ve seen the service act as a mediator (Mediator Pattern) between different parts of the system, promoting code reuse and reducing errors. By acting as a facade (Facade Pattern), it provides a clear business API. This means a trigger, an LWC, or an external API can call the same service, ensuring the logic is executed identically everywhere.

Unit of Work: Safe and Atomic Transactions

The Unit of Work pattern is, in my opinion, one of the gems of fflib. Its function is simple but revolutionary for Apex: it decouples business logic from DML operations.

Instead of performing an immediate insert or update, you register the intent to do so in an instance of fflib_SObjectUnitOfWork.

// Get an instance of the Unit of Work
fflib_ISObjectUnitOfWork uow = Application.UnitOfWork.newInstance();

// Register operations instead of executing them
uow.registerNew(newAccount);
uow.registerDirty(existingContact);
uow.registerDeleted(oldOpportunity);

// ... more business logic ...

// Finally, commit all changes in a single transaction
uow.commitWork();

My Opinion and Experience: The Unit of Work gives us two key benefits: transactional atomicity and DML efficiency. It groups all operations of the same type (e.g., all Account inserts) into a single DML call, which is essential for staying within governor limits. Even more important is atomicity. A team I worked with reported that the UoW ensured their data consistency by grouping operations into a single transaction and making rollbacks incredibly easy.

However, it’s crucial to understand this atomicity in bulk processing scenarios. Its nature is strictly “all or nothing.” This means if a single record in a batch of 200 fails validation, the entire transaction is rolled back, and no records are saved. It’s not that it doesn’t “bulkify” in terms of DML efficiency, but its goal is total integrity over partial success. Therefore, when you need to process records in bulk and prefer to save the valid ones while setting aside the errored ones, the right strategy often involves designing services that handle smaller batches or using native methods like Database.insert(records, allOrNone: false) outside of the UoW pattern.

The Power of Mocking: True Unit Testing in Apex

This is where fflib truly shines and justifies any learning curve. Thanks to the separation of layers and the use of interfaces, the fflib Apex Mocks framework allows us to write truly isolated unit tests.

We can test a service’s logic without touching the database. How? By “mocking” its dependencies. We tell the test: “When the service calls the LeadsSelector, don’t go to the database; instead, return this list of Leads that I’ve prepared.”

This allows us to verify the pure logic of our service quickly and deterministically. Tests run in milliseconds instead of seconds, drastically speeding up CI/CD cycles.

Conclusion: fflib as a Framework, Not a Dogma

I’ve seen teams get overwhelmed trying to implement fflib 100% from day one. My advice is to see it as a framework of tools, not an inflexible dogma.

You don’t need to adopt it all at once. Start where it adds the most value. In my experience, the most pragmatic path is:

  1. Start with Selectors and Services: Organizing queries and business logic is already a huge win.
  2. Introduce the Unit of Work: Once services are in place, managing DML centrally is the natural next step.
  3. Adopt Domains gradually: Refactoring triggers to use the Domain pattern can be the last step, as it often requires a more coordinated effort.

At the end of the day, fflib provides a proven architectural standard that leads to more maintainable, scalable, and robust systems. Success isn’t about blindly following rules but about understanding the principles behind them and applying them with good judgment to build high-quality software on the Salesforce platform. The reward is code you can be proud of and more agile development in the long run.