Skip to main content
01 Separation of ConcernsChapter 11 of 2020 Force DI
Chapter 11 · Implementation

Implementing The Domain Layer with the Apex Common Library

How to implement the Domain Layer with the assistance of the Apex Common Library.

13 min readVideo 38:50Implementation template
What you'll end up with
newCases.cls48
newCaseTrigger.trigger4
editApplication.cls+3
No interface needed

Domains don't need an interface. fflib_SObjectDomain is already the abstraction layer that you will extend in your domain classes (as we'll see below), and the application class (our factory) is what makes a domain mockable - you register a domain against the SObjectType.

If you've browsed the official fflib sample app you'll see per-object interfaces like IOpportunities and IOpportunitiesSelector on every domain and selector. That convention predates the Apex Stub API, which is perfectly happy mocking concrete classes - it's why this guide doesn't ask you to write them. Don't let the sample code convince you they're required. They are not, they just exist as references for older implementations should people require them.


The template for every Domain Class you create

Every Domain layer class you create for an object should at minimum have the following logic in it for it to work as expected.

//All domain classes should utilize inherited sharing so that the caller determines whether it should operate in system context or not. The should
//also extend the fflib_SObjectDomain class
public inherited sharing class Cases extends fflib_SObjectDomain{

//The constructor should always accept a list of the SObject type we're creating the domain class for
//It should then pass this list to the fflib_SObjectDomain class's constructor which is what super(cases) does.
//This sets the records value in the fflib_SObjectDomain class which is very important
public Cases(List<Case> cases){
super(cases);
}

//The name of this inner class must always be Constructor to work appropriately. This acts as a way to use the concept of reflection when initializing
//this class, despite the fact apex still does not support it.
public class Constructor implements fflib_SObjectDomain.IConstructable {
public fflib_SObjectDomain construct(List<SObject> sObjectList) {
return new Cases(sObjectList);
}
}
}

To understand why the Constructor inner class is necessary in these classes check out the triggerHandler method in the fflib_SObjectDomain class here: fflib_SObjectDomain triggerHandler method


Trigger Implementations using the Apex Common Library's Domain Layer

If you didn't know already, triggers should ideally have no logic in them... ever. Triggers do not support methods, and are extremely difficult to test accurately, which is why we instead write the code in trigger handlers or domain classes. Thankfully this concept has also been built into the Apex Common Library. To call the Domain Layer class you have built for your object in your trigger, just do the following:

//Note that I like to use the _Trigger in my trigger names, this is just personal preference as it makes it easier to discern it's a trigger
trigger NameOfDomainLayerClass_Trigger on YourObject (before insert, before update, after insert, after update)
{
//This trigger handler method eventually calls the Construct inner class of your Domain class to construct a version of your class
//and implement the logic in it
fflib_SObjectDomain.triggerHandler(NameOfDomainLayerClass.class);
}

How to Access the Trigger variables in your Domain Class

Technically, you could leverage trigger.new, trigger.oldMap and so on in your domain class... however you shouldn't for two reasons. The first reason is you will likely (at some point) want to call some aspects of your Domain class from outside a trigger context. If your Domain relies on the trigger context to operate, that's less than ideal. The second reason is you can't mock the trigger context, so a lot of the benefit of setting up this separation of concerns will be lost. In short, never access trigger context variables directly in your domain class.

Now you might be wondering, "This Domain class is supposed to be able to run in trigger context... I need to access those variables!!". No worries, you can still access them when you need them. If you've worked in SF long enough, with time you start to learn the only trigger context variables you need access to are trigger.new and trigger.oldMap. The rest typically really shouldn't be used. Trust me... you don't need them, unless you're building your own trigger handler setup.

So how do you actually get access to trigger.oldMap and trigger.new? Well that requires us to take a closer look at the triggerHandler method in the fflib_SObjectDomain class that our actual triggers call (example just above this section).

Basically when our trigger calls that triggerHandler method, it eventually runs the code below (source code here):

if(isInsert) domainObject = domainConstructor.construct(newRecords);
else if(isUpdate) domainObject = domainConstructor.construct(newRecords);
else if(isDelete) domainObject = domainConstructor.construct(oldRecordsMap.values());
else if(isUndelete) domainObject = domainConstructor.construct(newRecords);

The code above essentially passes trigger.new to the Records variable in the fflib_SObjectDomain class your Domain class extends when you are doing an insert, update or undelete operation; and it passes in trigger.oldMap.values to the Records variable if you are doing a delete operation.

Ok that's cool, but how do you access trigger.oldMap when you need it?? Well, the only time you need trigger.oldMap is in update operations, so that's the only time it's accessible. When you set up your onBeforeUpdate or onAfterUpdate methods in your Domain class you'll set them up like what you see below:

public override void onBeforeUpdate(Map<Id, SObject> existingRecords){
//existingRecords is trigger.oldMap
}

In trigger context when onBeforeUpdate gets called, trigger.oldMap is passed in to the existingRecords variable and you're free to use it as you please.

There you have it! That's it! Simpler than you maybe thought... maybe, lol.


The fflib_SObjectDomain Class Methods Cheat Sheet

While there are many other accessible methods in the fflib_SObjectDomain class below are the methods most commonly utilized in implementations.

  1. onApplyDefaults() - This method is called in the handleBeforeInsert method and exists so that you can apply default logic that is applicable to all new records that are created in the system.
  2. onValidate() - This method is called in the handleAfterInsert method and exists so that you can apply validation logic to your inserted records before committing them to the database.
  3. onValidate(Map<Id, SObject> existingRecords) - This method is called in the handleAfterUpdate method and exists so that you can apply validation logic to your updated records before committing them to the database.
  4. onBeforeInsert() - This method is called in the handleBeforeInsert method and exists so that you can override it to place logic that should occur during a before insert action in a trigger.
  5. onBeforeUpdate(Map<Id, SObject>) - This method is called in the handleBeforeUpdate method and exists so that you can override it to place logic that should occur during a before update action in a trigger.
  6. onBeforeDelete() - This method is called in the handleBeforeDelete method and exists so that you can override it to place logic that should occur during a before delete action in a trigger.
  7. onAfterInsert() - This method is called in the handleAfterInsert method and exists so that you can override it to place logic that should occur during an after insert action in a trigger.
  8. onAfterUpdate(Map<Id, SObject>) - This method is called in the handleAfterUpdate method and exists so that you can override it to place logic that should occur during an after update action in a trigger.
  9. onAfterDelete() - This method is called in the handleAfterDelete method and exists so that you can override it to place logic that should occur during an after delete action in a trigger.
  10. onAfterUndelete() - This method is called in the handleAfterUndelete method and exists so that you can override it to place logic that should occur during an after undelete action in a trigger.
  11. handleBeforeInsert() - This method is called in the triggerHandler method when a beforeInsert is happening in the trigger. By default it calls the onApplyDefaults method and the onBeforeInsert method but it can be overridden and implemented in a different way if desired.
  12. handleBeforeUpdate(Map<Id, SObject>) - This method is called in the triggerHandler method when a beforeUpdate is happening in the trigger. By default it calls the onBeforeUpdate method but it can be overridden and implemented in a different way if desired.
  13. handleBeforeDelete() - This method is called in the triggerHandler method when a beforeDelete is happening in the trigger. By default it calls the onBeforeDelete method but it can be overridden and implemented in a different way if desired.
  14. handleAfterInsert() - This method is called in the triggerHandler method when an afterInsert is happening in the trigger. By default it calls the onValidate and onAfterInsert methods but it can be overridden and implemented in a different way if desired.
  15. handleAfterUpdate() - This method is called in the triggerHandler method when an afterUpdate is happening in the trigger. By default it calls the onValidate and onAfterUpdate methods but it can be overridden and implemented in a different way if desired.
  16. handleAfterDelete() - This method is called in the triggerHandler method when an afterDelete is happening in the trigger. By default it calls the onAfterDelete method but it can be overridden and implemented in a different way if desired.
  17. handleAfterUndelete() - This method is called in the triggerHandler method when an afterUndelete is happening in the trigger. By default it calls the onAfterUndelete method but it can be overridden and implemented in a different way if desired.
  18. getChangedRecords(Set<String> fieldNames) - This method will return a list of records that have had their fields changed (the fields specified in the method parameter passed in).
  19. getChangedRecords(Set<Schema.SObjectField> fieldTokens) - This method will return a list of records that have had their fields changed (the fields specified in the method parameter passed in). I would suggest using this method over the one above. Strongly typed field names are a better choice in my opinion so the system knows your code depends on that field.

Only run your logic against records that actually changed

The two getChangedRecords methods at the bottom of that cheat sheet deserve more than a line item. In an update trigger, Records hands you every record in the trigger context, whether or not the fields your logic cares about were touched. Filtering down to just the records that changed before you do anything expensive is one of the cheapest optimizations available to you - the queries, loops and DML downstream of the filter simply don't run when nothing relevant changed.

public override void onAfterUpdate(Map<Id, SObject> existingRecords){
//Only the cases whose Status actually changed, not every updated case
List<Case> statusChangedCases = (List<Case>) getChangedRecords(
new Set<Schema.SObjectField>{ Case.Status });

if(statusChangedCases.isEmpty()){
return;
}

//Everything below here (and every query and DML statement it triggers)
//only executes when a Status value really did change
}

Testing a Domain Class without a single DML statement

Here's a capability of fflib_SObjectDomain that far too few people know exists: it ships with its own built-in mock database for testing. The Test.Database inner class lets you seed records, run your real trigger handler sequence against them, and assert the results - with zero actual DML. No inserted records, no dependent data setup, no waiting on the database. Your domain validation tests go from seconds to milliseconds, and you can afford to test far more input scenarios than you ever would when every test method pays the DML tax.

There are three pieces to it.

1) Seed the mock database instead of inserting records:

fflib_SObjectDomain.Test.Database.onInsert(new Case[]{ testCase });

There are matching onUpdate and onDelete methods for emulating those DML operations as well.

2) Invoke the trigger handler directly from your test. This is the exact same one-line call your actual trigger makes, so the full handler sequence (onApplyDefaults, onValidate, your onBeforeInsert override, and so on) runs in the correct order against your mock records:

fflib_SObjectDomain.triggerHandler(Cases.class);

3) Register errors with the error method instead of calling addError directly. This is a small convention change in your domain class that makes your validations assertable. Instead of this:

record.Subject.addError('A subject is required.');

Do this:

record.Subject.addError(error('A subject is required.', record, Case.Subject));

The error method registers the message in a request-scoped error list (much like ApexPages.getMessages(), it accumulates across every domain class that executes during the request), and your test can then assert exactly which errors were raised and exactly which fields they landed on - no more wrapping DML in try/catch and doing a e.getMessage().contains('the error I hope is in here') string search against the exception text.

Put together, a complete domain validation test looks like this:

@IsTest
private static void onValidate_MissingSubject_UnitTest(){
//Seed the mock database (no DML happens here)
Case testCase = new Case(Status = 'New');
fflib_SObjectDomain.Test.Database.onInsert(new Case[]{ testCase });

//Run the real trigger handler sequence, exactly like CaseTrigger would
fflib_SObjectDomain.triggerHandler(Cases.class);

//Assert against the error registry instead of catching exceptions
System.assertEquals(1, fflib_SObjectDomain.Errors.getAll().size());
System.assertEquals('A subject is required.',
fflib_SObjectDomain.Errors.getAll()[0].message);
System.assertEquals(Case.Subject,
((fflib_SObjectDomain.FieldError) fflib_SObjectDomain.Errors.getAll()[0]).field);
}
The error convention costs you nothing

The error method convention still works exactly as you'd expect under real DML and real trigger execution - the error is attached to the record/field just like a plain addError call. Adopting it isn't an either/or decision; it simply makes your domain testable both ways.

This harness is complementary to, not a replacement for, the mocking you'll learn in the Apex Mocks chapters - Apex Mocks stubs out a domain class's dependencies, while Test.Database exercises the domain's own trigger logic without the database. And to be clear, it doesn't replace integration tests either: you still need tests that do real DML to prove the whole stack works together (more on that split in chapter 15).


The Configuration Inner Class for fflib_SObjectDomain (Setting trigger state and trigger security)

Inside the fflib_SObjectDomain class you'll find an inner class called Configuration. This inner class allows you to enable and disable Trigger State as well as enable and disable CRUD security in your trigger. By default trigger state is disabled and CRUD security is enforced.

The Configuration calls belong in your domain class's own constructor, right after the super(records) call - that way the configuration applies every time the class is constructed, no matter which trigger phase (or test) constructed it:

public inherited sharing class Cases extends fflib_SObjectDomain{

public Cases(List<Case> cases){
super(cases);
//Configuration calls go here, after super()
Configuration.enableTriggerState();
}
}

Trigger State

To understand what this switch does, you need to know one thing about how the trigger handler works: it constructs a brand new instance of your domain class for each trigger phase. A member variable you set during onBeforeInsert is gone by the time onAfterInsert runs, because the after phase is executing against a fresh instance.

Configuration.enableTriggerState() changes that - the handler holds on to the instance created in the before phase and reuses it in the after phase, so member variables survive:

public inherited sharing class Cases extends fflib_SObjectDomain{

public String someState;

public Cases(List<Case> cases){
super(cases);
Configuration.enableTriggerState();
}

public override void onBeforeInsert(){
System.assertEquals(null, someState);
//Maybe an expensive query result you want to reuse in the after phase
someState = 'Something';
}

public override void onAfterInsert(){
//Still here - same instance, thanks to trigger state
System.assertEquals('Something', someState);
}
}

The feature is also trigger-recursion-aware: if your DML causes the trigger to fire again, the handler recognizes it's dealing with a new set of records and creates a fresh domain instance for them rather than handing the recursive invocation your stateful one.

This is also the switch to reach for when migrating legacy trigger code. If your org's old trigger handlers are littered with static Boolean variables being used to smuggle state between the before and after phases (or to guard against recursion), trigger state is the framework-supported replacement for that pattern.

How to turn trigger state on and off using the Configuration inner class:

//Turn on
Configuration.enableTriggerState();
//Turn off
Configuration.disableTriggerState();

Enforcing CRUD

The enforcing trigger CRUD (Create, Read, Update, Delete) ensures that a user has the appropriate object CRUD permissions before performing any actual DML actions. By default in the fflib_SObjectDomain class this is enforced. Ideally you should leave this as enforced unless you have a really excellent business reason to not enforce it - for instance, a logging object that every user needs to write to regardless of their permissions, where you've deliberately decided the calling code owns the security check:

public inherited sharing class ApplicationLogs extends fflib_SObjectDomain{

public ApplicationLogs(List<Application_Log__c> logs){
super(logs);
//This object is written to in system context by design;
//the calling code is responsible for security checks
Configuration.disableTriggerCRUDSecurity();
}
}

How to turn CRUD enforcement on and off using the Configuration inner class:

//Enable CRUD
Configuration.enforceTriggerCRUDSecurity();
//Disable CRUD
Configuration.disableTriggerCRUDSecurity();

The Trigger Event Inner Class (Turning trigger events on and off)

Before looking at the trigger event methods, it's worth knowing the problem it was built to kill. In orgs that grew through years of iterative projects, you'll almost always find some version of this hand-rolled in a trigger handler:

public override void onAfterInsert(){
//If this is set we are already in a loop and want to exit!
if(prohibitAfterInsertTrigger){
return;
}
//Actual logic down here
}

Small and inconspicuous - but every domain class ends up with its own copy, each with its own slightly different flag names, and there's no control over where those flags get flipped from. The code base drifts into inconsistency one boolean at a time. The TriggerEvent inner class replaces all of that with a set of methods that are easy to utilize, one line, that every developer on the team can use and find the same way.

Inside the fflib_SObjectDomain class is an inner class called TriggerEvent that allows you to turn on and off the various trigger events at will. By default all trigger events are turned on.

Example Code for shutting down and re-enabling a portion of a domain trigger

//Disables the before insert portion of the trigger
fflib_SObjectDomain.getTriggerEvent(Cases.class).disableBeforeInsert();
//Code to execute while that trigger event is off
//Re-enables the before insert portion of the trigger
fflib_SObjectDomain.getTriggerEvent(Cases.class).enableBeforeInsert();

The following is a list of trigger event methods and what they do:

  1. TriggerEvent.enableBeforeInsert() - This method enables the before insert portion of the trigger.

  2. TriggerEvent.enableBeforeUpdate() - This method enables the before update portion of the trigger.

  3. TriggerEvent.enableBeforeDelete() - This method enables the before delete portion of the trigger.

  4. TriggerEvent.disableBeforeInsert() - This method disables the before insert portion of the trigger.

  5. TriggerEvent.disableBeforeUpdate() - This method disables the before update portion of the trigger.

  6. TriggerEvent.disableBeforeDelete() - This method disables the before delete portion of the trigger.

  7. TriggerEvent.enableAfterInsert() - This method enables the after insert portion of the trigger.

  8. TriggerEvent.enableAfterUpdate() - This method enables the after update portion of the trigger.

  9. TriggerEvent.enableAfterDelete() - This method enables the after delete portion of the trigger.

  10. TriggerEvent.enableAfterUndelete() - This method enables the after undelete portion of the trigger.

  11. TriggerEvent.disableAfterInsert() - This method disables the after insert portion of the trigger.

  12. TriggerEvent.disableAfterUpdate() - This method disables the after update portion of the trigger.

  13. TriggerEvent.disableAfterDelete() - This method disables the after delete portion of the trigger.

  14. TriggerEvent.disableAfterUndelete() - This method disables the after undelete portion of the trigger.

  15. TriggerEvent.enableAll() - This method enables all portions of the trigger.

  16. TriggerEvent.disableAll() - This method disables all portions of the trigger.

  17. TriggerEvent.enableAllBefore() - This method enables all before portions of the trigger.

  18. TriggerEvent.disableAllBefore() - This method disables all before portions of the trigger.

  19. TriggerEvent.enableAllAfter() - This method enables all after portions of the trigger.

  20. TriggerEvent.disableAllAfter() - This method disables all after portions of the trigger.


Example Apex Common Implementation of a Domain Class

Cases Domain Layer Example

Contacts Domain Layer Example