SoC and the Apex Common Library Tutorial Series Part 5: The Unit of Work Pattern

What is the Unit of Work Pattern (UOW)

A Unit of Work, “Maintains a list of objects affected by a business transaction and coordinates the writing out of changes and the resolution of concurrency problems”.

The goal of the unit of work pattern is to simplify DML in your code and only commit changes to the database/objects when it’s truly time to commit. Considering the many limits around DML in Salesforce, it’s important to employ this pattern in your org in some way. It’s also important to note that this, “maintains a list of objects affected by a business transaction”, which indicates that the UOW pattern should be prevalent in your service layer (The service layer houses business logic).

The UOW pattern also ensures we don’t have data inconsistencies in our Salesforce instance. It does this by only committing work when all the DML operations complete successfully. It rolls back our transactions when any DML fails in our unit of work.


Benefits of the using the Unit of Work Pattern in Salesforce

There are several, but here are the biggest of them all… massive amounts of code reduction, having consistency with your DML transactions, doing the minimal DML statements feasible (bulkification) and DML mocking in unit tests. Let’s figure out how we reduce the code and make it more consistent first.

The Code Reduction and Consistency

Think about all the places in your codebase where you insert records, error handle the inserting of your records and manage the transactional state of your records (Savepoints). Maybe if your org is new there’s not a ton happening yet, but as it grows the amount of code dealing with that can become enormous and, even worse, inconsistent. I’ve worked in 12 year old orgs that had 8000+ lines of code just dedicated to inserting records throughout the system and with every dev who wrote the code a new variety of transaction management took place, different error handling (or none at all), etc.

Code Bulkification

The unit of work pattern also helps a great deal with code bulkification. It encourages you to to finish creating and modifying 100% of your records in your transaction prior to actually committing them (doing the dml transactions) to the database (objects). It makes sure that you are doing that absolute minimal transactions necessary to be successful. For instance, maybe for some reason in your code you are updating cases in one method, and when you’re done you call another method and it updates those same cases… why do that? You could register all those updates and update all those cases at once with one DML statement. Whether you realize it at the time or not, even dml statement counts… use them sparingly.

DML Mocking for Unit Tests

If you’re not sure what mocking and unit test are, then definitely check out my section on that in the wiki here. Basically, in an ideal scenario you would like to do unit testing, but unit testing depends on you having the ability to mock classes for you tests (basically creating fake versions of your class you have complete control over in your tests). Creating this layer that handles your dml transactions allows you to mock that layer in your classes when doing unit tests… If this is confusing, no worries, we’ll discuss it a bunch more later in the last three sections of this wiki.


Next Section

Part 6: The fflib_SObjectUnitOfWork Class

SoC and the Apex Common Library Tutorial Series Part 6: The fflib_SObjectUnitOfWork Class


What is the fflib_SObjectUnitOfWork class?

It is a foundation built to allow you to leverage the unit of work design pattern from within Salesforce. Basically this class is designed to hold your database operations (insert, update, etc) in memory until you are ready to do all of your database transactions in one big transaction. It also handles savepoint rollbacks to ensure data consistentcy. For instance, if you are inserting Opportunities with Quotes in the same database (DML) transaction, chances are you don’t wanna insert those Opportunities if your Quotes fail to insert. The unit of work class is setup to automatically handle that transaction management and roll back if anything fails.

If also follows bulkification best practices to make your life even easier dealing with DML transactions.


Why is this class used?

This class is utilized so that you can have super fine control over your database transactions and so that you only do DML transactions when every single record is prepped and ready to be inserted, updated, etc.

Additionally there are two reasons it is important to leverage this class (or a class like it):
1) To allow for DML mocking in your test classes.
2) To massively reduce duplicate code for DML transactions in your org.
3) To make DML transaction management consistent

Think about those last two for a second… how many lines of code in your org insert, update, upsert (etc) records in your org? Then think about how much code also error handles those transaction and (if you’re doing things right) how much code goes into savepoint rollbacks. That all adds up over time to a ton of code. This class houses it all in one centralized apex class. You’ll never have to re-write all that logic again.


How to Register a Callback method for an Apex Commons UOW

The following code example shows you how to setup a callback method for your units of work using the fflib_SObjectUnitOfWork.IDoWork interface, should you need them.

public inherited sharing class HelpDeskAppPostCommitLogic implements fflib_SObjectUnitOfWork.IDoWork{
    List<Task> taskList;
    
    public HelpDeskAppPostCommitLogic(List<Task> taskList){
        this.taskList = taskList; 
    }
    
    public void doWork(){
        //write callback code here
    }
}

The code below shows you how to actually make sure your unit of work calls your callback method.

fflib_ISObjectUnitOfWork uow = Helpdesk_Application.helpDeskUOW.newInstance();
//code to create some tasks
uow.registerNew(newTasks);
uow.registerWork(new HelpDeskAppPostCommitLogic(newTasks));
uow.commitWork();    

Apex Commons Unit of Work Limitations

1) Records within the same object that have lookups to each other are currently not supported. For example, if the Account object has a Lookup to itself, that relationship cannot be registered.

2) You cannot do all or none false database transactions without creating a custom IDML implementation.

Database.insert(acctList, false);

3) To send emails with the Apex Commons UOW you must utilize the special registerEmail method.

4) It does not manage FLS and CRUD without implementing a custom class that implements the IDML interface and does that for you.

To do these things in your own way you would need to make a new class that implements the fflib_SObjectUnitOfWork’s IDML interface which we’ll cover below


How and When to use the fflib_SObjectUnitOfWork IDML Interface

If your unit of work needs a custom implementation for inserting, updating, deleting, etc that is not supported by the SimpleDML inner class then you are gonna want to create a new class that implements the fflib_SObjectUnitOfWork.IDML interface. After you create that class if you were using the Application factory you would instantiate your unit of work like so Application.uow.newInstance(new customIDMLClass()); otherwise you would initialize it using public static fflib_SObjectUnitOfWork uow = new fflib_SObjectUnitOfWork(new List<SObjectType>{Case.SObjectType}, new customIDMLClass());. A CUSTOM IDML CLASS IS SUPER IMPORTANT IF YOU WANT TO MANAGE CRUD AND FLS!!! THE fflib_SObjectUnitOfWork class does not do that for you! So let’s check out an example of how to implement a custom IDML class together below.

Example of an IDML Class

//Implementing this class allows you to overcome to limitations of the regular unit of work class.
public with sharing class IDML_Example implements fflib_SObjectUnitOfWork.IDML
{
    public void dmlInsert(List<SObject> objList){
        //custom insert logic here
    }
    public void dmlUpdate(List<SObject> objList){
        //custom update logic here
    }
    public void dmlDelete(List<SObject> objList){
        //custom delete logic here
    }
    public void eventPublish(List<SObject> objList){
        //custom event publishing logic here
    }
    public void emptyRecycleBin(List<SObject> objList){
        //custom empty recycle bin logic here
    }
}

fflib_SObjectUnitOfWork class method cheat sheet

This does not encompass all methods in the fflib_SObjectUnitOfWork class, however it does cover the most commonly used methods. There are also methods in this class to publish platform events should you need them but they aren’t covered below.

1) registerNew(SObject record) Registers a single record as a new record that need to be inserted.
2)
registerNew(List<SObject> records) – Registers a list of records as new records that need to be inserted.
3)
registerNew(SObject record, Schema.SObjectField relatedToParentField, SObject relatedToParentRecord) Registers a new record that needs to be inserted with a parent record relationship (this parent needs to have also been registered as a new record in your unit of work).
4)
registerRelationship(SObject record, Schema.SObjectField relatedToField, SObject relatedTo) Registers a relationship between two records that have yet to be inserted into the database. Both records need to be registered in your unit of work.
5)
registerRelationship( Messaging.SingleEmailMessage email, SObject relatedTo ) This method will allow you to register a relationship between an email message and a record. Both the email message and the record need to be registered in your unit of work to allow this to work.
6)
registerRelationship(SObject record, Schema.SObjectField relatedToField, Schema.SObjectField externalIdField, Object externalId) This method can be used to register a relationship between one record and another using an external id field. There is an example of how to implement this in the comments for this method linked above.
7)
registerDirty(SObject record) Registers a single record to be updated.
8) registerDirty(List records, List dirtyFields) This method should be used if you believe you’ve already registered a list of records to be updated by your unit of work and some of that records fields have been updated. This basically merges those new field updates into your already registered record.
9)
registerDirty(SObject record, List dirtyFields) This method should be used if you believe you’ve already registered a record to be updated by your unit of work and some of that records fields have been updated. This basically merges those new field updates into your already registered record.
10)
registerDirty(SObject record, Schema.SObjectField relatedToParentField, SObject relatedToParentRecord) This method is used to register an update to a record while also registering a new relationship to another record that has been registered as a new record in the same unit of work.
11)
registerDirty(List<SObject> records) This method is used to register a list of records to be updated.
12)
registerUpsert(SObject record) This method is used to register a single record to be upserted.
13) registerUpsert(List<SObject> records) This method is used to register a list of records for an upsert.
14) registerDeleted(SObject record) Registers a single record to be deleted.
15) registerDeleted(List<SObject> records) Registers a list of records to be deleted.
16)
registerPermanentlyDeleted(List<SObject> records) Registers a list of records to be permanently deleted. Basically it deletes records and then removes them from the recycle bin as well.
17)
registerPermanentlyDeleted(SObject record) Registers a record to be permanently deleted from the org. Basically it deletes records and then removes them from the recycle bin as well.
18)
registerEmptyRecycleBin(SObject record) This registers a record to be permanently deleted from the system by both deleting it and emptying it from the recycle bin.
19) public void registerEmptyRecycleBin(List<SObject> records) This takes a list of records and permanently deletes them from the system.
20) registerEmail(Messaging.Email email) Registers an email message to be sent
21) registerWork(IDoWork work) Registers a callback method to be called after your work has been committed to the database.
22) commitWork() Commits your unit of work (records registered) to the database. This should always be called last.


Next Section

Part 7: The Service Layer