Skip to main content
Build the future with Agentforce at TDX in San Francisco or on Salesforce+ on March 5–6. Register now.

Apply Domain Layer Principles in Apex

Learning Objectives

After completing this unit, you’ll be able to:

  • Create a Domain Apex class.
  • Reflect your defaulting and validation code in a Domain class.
  • Map methods in the Domain class to Apex trigger events.
  • Control the application of security enforcement at runtime.

Follow Along with Trail Together

Want to follow along with an expert as you work through this step? Take a look at this video, part of the Trail Together series.

(This clip starts at the 17:30 minute mark, in case you want to rewind and watch the beginning of the step again.)

Reference Code

This module references these Apex classes from the FFLIB Apex Common Samplecode project. You may want to open them before you begin.

Creating Domain Classes

The fflib_SObjectDomain class used in the trigger code in the previous unit extends a base class supporting trigger-handler functionality and provides useful functionality like object security.

fflib_SObjectDomain class.

Note

Note: Most methods exist as virtual methods. The handle*XXXX* methods in this base class are responsible for ensuring that the on*XXXX* methods are called at the appropriate times. For more advanced cases in which you need direct handling, you may override handler methods, but be aware that unless you call the superclass version of the handle*XXX method, the superclass’ on*XXXX* methods aren’t called. For example, refer to the OpportunitiesTriggerHandler class methods for various examples.

The base class uses the template method pattern to provide standard hooks to implement common domain logic for validation of records via the onValidate() method and defaulting field values via the onApplyDefaults() method.

There are also methods for placing logic relating to specific Apex trigger events. Finally, the constructor (for which all classes that extended the fflib_SObjectDomain class must also expose) takes a list of sObjects per the bulkification design goal described in the previous unit.

Here is a basic implementation of the Opportunities domain class.

public class Opportunities extends fflib_SObjectDomain {
    public Opportunities(List<Opportunity> sObjectList) {
        super(sObjectList);
    }
    public class Constructor implements fflib_SObjectDomain.IConstructable {
        public fflib_SObjectDomain construct(List<SObject> sObjectList) {
            return new Opportunities(sObjectList);
        }
    }
}

Notice that the constructor inner class allows the base class method fflib_SObjectDomain.triggerHandler, used in the Apex trigger sample shown in the previous unit, to create a new instance of the trigger handler logic for a domain class and passes in the sObject list, typically Trigger.new.

Implementing Field Defaulting Logic

To provide a place for field defaulting logic, the fflib_SObjectDomain base class exposes the onApplyDefaults() method. This method is called from the handleBeforeInsert() method in the fflib_SObjectDomain base class during a trigger invocation.

Placing logic here ensures that defaulting occurs consistently across the application when records are added. You can also call it explicitly, if needed, from a service that helps present default record values to a user accessing a custom UI via a Visualforce page or Lightning component, for example.

The base class exposes the sObject list provided during the constructor call to all methods via the records property. Although we’re not strictly in a trigger scenario here, it’s still strongly encouraged to consider bulkification, per the Domain design goals in the previous unit.

public override void onApplyDefaults() {
    // Apply defaults to Opportunities
    for(Opportunity opportunity : (List<Opportunity>) Records) {
        if(opportunity.DiscountType__c == null) {
            opportunity.DiscountType__c = OpportunitySettings__c.getInstance().DiscountType__c;
        }               
    }
}
Note

Note: The above example is also possible through using a formula expressed as part of the DiscountType__c field definition. Of course, defaulting logic could span multiple fields or other records. In that case, you must resort to Apex coding.

Implementing Validation Logic

Although you can override any of the above trigger methods to implement validation logic, it’s best practice to do this only in the after phase of the Apex trigger invocation. By overriding one of the two onValidate() methods of the fflib_SObjectDomain base class, you can implement this logic in a clearly defined place.

public override void onValidate() {
    // Validate Opportunities
    for(Opportunity opp : (List<Opportunity>) this.records) {
        if(opp.Type.startsWith('Existing') && opp.AccountId == null) {
            opp.AccountId.addError('You must provide an Account when ' +
                'creating Opportunities for existing Customers.');
        }
    }
}

The above onValidate() method is called from the base class when records are inserted on the object. If you require validation logic that is sensitive to data changing during record updates, you can override the following variant.

public override void onValidate(Map<Id,SObject> existingRecords) {
    // Validate changes to Opportunities
    for(Opportunity opp : (List<Opportunity>) Records) {
        Opportunity existingOpp = (Opportunity) existingRecords.get(opp.Id);
        if(opp.Type != existingOpp.Type) {
            opp.Type.addError('You cannot change the Opportunity type once it has been created');
        }
    }
}

Notice that the code in the base class methods handleAfterInsert() and handleAfterUpdate() ensures that security best practice is enforced by calling this method only during the after part of the Apex trigger (after all Apex triggers on this object have completed). This behavior is most important to AppExchange package developers (see the Resources section).

Implementing Apex Trigger Logic

It’s not always the case that the domain logic you implement falls into the above methods. Indeed, it’s not a strict requirement to implement all defaulting or validation logic in these methods, per the Separation of Concerns guidelines. It’s purely a consideration. If you prefer, you could put it all in the methods below.

To implement Apex trigger-related code that invokes behaviors via other domain objects, the following is a slightly contrived example of overriding the onAfterInsert() method to update the Description field on related Accounts whenever new Opportunities are inserted.

public override void onAfterInsert() {
    // Related Accounts
    List<Id> accountIds = new List<Id>();
    for(Opportunity opp : (List<Opportunity>) Records) {
        if(opp.AccountId!=null) {
            accountIds.add(opp.AccountId);
        }
    }
    // Update last Opportunity activity on related Accounts via the Accounts domain class
    fflib_SObjectUnitOfWork uow =
      new fflib_SObjectUnitOfWork(
        new Schema.SObjectType[] { Account.SObjectType });
    Accounts accounts = new Accounts([select Id from Account
      where id in :accountIds]);
    accounts.updateOpportunityActivity(uow);
    uow.commitWork();              
}

Some things to note about the above example.

  • An instance of the Accounts Domain class is initialized using an inline SOQL query. The next unit in this module introduces a pattern that helps encapsulate query logic for better reuse and consistency around the resulting data, which is important to the Domain class logic.
  • The fflib_SObjectUnitOfWork instance is used in an Apex trigger context rather than a service context per the SOC module. In this case, its scope is a trigger event or method. These methods are directly called by the platform, not the service layer. As such a Unit of Work is created and given to the Accounts method for it to register updates to the Account records. Although not shown here, it’s typically good practice to have a single place to initialize the Unit of Work to avoid duplication.
  • Delegation to the Accounts domain class is appropriate here because updating activity based on Accounts is more a behavior of the Account object rather than Opportunity. This type of SOC between Domain classes is also illustrated later in the next section.

For reference, here is the Accounts.updateOpportunityActivity method.

public class Accounts extends fflib_SObjectDomain {
    public Accounts(List<Account> sObjectList) {
        super(sObjectList);
    }
    public void updateOpportunityActivity(fflib_SObjectUnitOfWork uow) {
        for(Account account : (List<Account>) Records) {
            account.Description = 'Last Opportunity Raised ' + System.today();
            uow.registerDirty(account);
        }
    }
}

Implementing Custom Logic

You are not restricted to implementing only methods that are overridable from the base class. Recall the revised Service layer shown in the previous unit.

public static void applyDiscounts(Set<Id> opportunityIds, Decimal discountPercentage) {
    // Unit of Work
    // ...
    // Validate parameters
    // ...
    // Construct Opportunities domain class
    Opportunities opportunities = new Opportunities( ... );
    // Apply discount via domain class behavior
    opportunities.applyDiscount(discountPercentage, uow);
    // Commit updates to opportunities     
    uow.commitWork();                      
}

This code uses a Domain class method that can apply a discount to an Opportunity, further encapsulating this logic in a place that’s associated with the Domain object.

When required, the code delegates to the OpportunityLineItems domain class to apply line-level discounts. For the sake of this discussion, assume that logic is different for opportunities that leverage product lines.

Code using a Domain class method that can apply a discount to an Opportunity.

You can view the code for the OpportunityLineItems domain class here.

The fflib_ISObjectUnitOfWork instance is taken as an argument so that the caller (in this case, the OpportunitiesService.applyDiscount method) can pass it in for the Domain code to register work against it and subsequently passed onto the OpportunityLineItems domain class’ applyDiscount() method.

Business Logic in the Domain Class vs. Service Class

Sometimes it can feel less than obvious where to place code. Let’s go back to the SOC principle and think about what the Service and Domain layers are concerned with.

Type of Application Concern Service or Domain Example
Making sure that fields are validated and defaulted consistently as record data is manipulated.
Domain Apply default discount policy to products as they are added.
Responding to a user or system action that involves pulling together multiple pieces of information or updating multiple objects. Mostly provides actions that can occur to a collection of records and coordinates everything that is needed to complete that action (potentially with other supporting services).
Service Create and calculate invoices from work orders. Might pull in price book information.
Handling changes to records that occur in the application as part of other related records changing or through the execution of a user or system action. For example, defaulting values as required. If changing one field affects another, it’s also updated.
Domain How the Account object reacts when an Opportunity is created or how the Discount is applied when the Opportunity discount process is run. Note: This sort of logic might start in the Service layer, but be better served in the Domain layer to manage the size and complexity of the service method size or improve reuse.
Handling of common behavior that applies across a number of different objects.
Domain Calculate price on the opportunity product or work order product lines. Note: You could place this in a shared Domain base class, overriding the fflib_SObjectDomain method to hook into the Apex trigger events, with concrete Domain classes extending this class in turn with their behaviors.

Controlling Security Enforcement

By default, the fflib_SObjectDomain base class enforces Salesforce object CRUD security. However, it is invoked for all types of access to the object, whether through a controller or service. Service logic might want to access an object on behalf of the user without requiring permissions to the object.

If you prefer to disable this default behavior and enforce it yourself in your service code, you can use a configuration feature of the base class. The following example shows how to do this in each constructor. Another way is to create your own base class with this code, and then extend that class for all Domain classes.

public Opportunities(List<Opportunity> sObjectList) {
    super(sObjectList);        
    // Disable default Object Security checking    
    Configuration.disableTriggerCRUDSecurity();
}  
Note

Note: The base class doesn’t offer generic support for enforcing field-level security for updates. This enforcement is still your responsibility.

Testing Domain Classes

Factoring logic into smaller, more encapsulated chunks is beneficial for test-driven development (TDD), because you can more easily construct the domain classes in your tests and invoke the methods directly. This does not mean you should not test your service layer, but it does allow you to apply a more incremental test-and-develop approach.

Get Ready for the Hands-on Challenge

To complete this challenge, launch the Trailhead playground you used in the Apex Enterprise Patterns: Service Layer module. You’ll need the open source libraries you already installed. If you’re using a different Trailhead playground, launch it and install the ApexMocks library first and then the Apex Commons library using the Deploy to Salesforce buttons below. You can read more about both of these libraries and their respective open source license agreements in their repos.

Deploy the ApexMocks open source library.

Deploy to Salesforce

Deploy the Apex Common open source library.

Deploy to Salesforce button.

Resources

Share your Trailhead feedback over on Salesforce Help.

We'd love to hear about your experience with Trailhead - you can now access the new feedback form anytime from the Salesforce Help site.

Learn More Continue to Share Feedback