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". - Martin Fowler
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 it "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 Using the Unit of Work Pattern in Salesforce
There are several, but here are the biggest of them all:
- Having consistency with your DML transactions
- Doing the fewest DML statements feasible (bulkification)
- DML mocking in unit tests.
- Code reduction
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. Think about all of those 12-year-old orgs out there that have 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 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 Unit of Work commitwork() method call. Whether you realize it at the time or not, every DML statement counts... use them sparingly.
DML Mocking for Unit Tests
If you're not sure what mocking and unit tests are, then definitely check out my chapter on that 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 your tests (basically creating fake versions of your class that 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 chapters of this guide.
The difference it makes in real code
Talk is cheap, so let's look at the same job done both ways. The task: create ten Opportunities, each with a variable number of Products, PricebookEntries and OpportunityLineItems - a chain of four objects that have to be inserted in dependency order, with each child stitched to a parent Id that doesn't exist until the previous insert finishes.
List<Opportunity> opps = new List<Opportunity>();
List<List<Product2>> productsByOpp = new List<List<Product2>>();
List<List<PricebookEntry>> entriesByOpp = new List<List<PricebookEntry>>();
List<List<OpportunityLineItem>> linesByOpp = new List<List<OpportunityLineItem>>();
for(Integer o = 0; o < 10; o++){
Opportunity opp = new Opportunity();
opp.Name = 'Opportunity ' + o;
opp.StageName = 'Open';
opp.CloseDate = System.today();
opps.add(opp);
List<Product2> products = new List<Product2>();
List<PricebookEntry> pricebookEntries = new List<PricebookEntry>();
List<OpportunityLineItem> oppLineItems = new List<OpportunityLineItem>();
for(Integer i = 0; i < o + 1; i++){
Product2 product = new Product2();
product.Name = opp.Name + ' : Product : ' + i;
products.add(product);
PricebookEntry pbe = new PricebookEntry();
pbe.UnitPrice = 10;
pbe.IsActive = true;
pbe.UseStandardPrice = false;
pbe.Pricebook2Id = standardPricebookId;
pricebookEntries.add(pbe);
OpportunityLineItem oppLineItem = new OpportunityLineItem();
oppLineItem.Quantity = 1;
oppLineItem.TotalPrice = 10;
oppLineItems.add(oppLineItem);
}
productsByOpp.add(products);
entriesByOpp.add(pricebookEntries);
linesByOpp.add(oppLineItems);
}
insert opps;
List<Product2> allProducts = new List<Product2>();
for(List<Product2> products : productsByOpp){
allProducts.addAll(products);
}
insert allProducts;
Integer oppIdx = 0;
List<PricebookEntry> allEntries = new List<PricebookEntry>();
for(List<PricebookEntry> pricebookEntries : entriesByOpp){
List<Product2> products = productsByOpp[oppIdx++];
Integer lineIdx = 0;
for(PricebookEntry pricebookEntry : pricebookEntries){
pricebookEntry.Product2Id = products[lineIdx++].Id;
}
allEntries.addAll(pricebookEntries);
}
insert allEntries;
oppIdx = 0;
List<OpportunityLineItem> allLines = new List<OpportunityLineItem>();
for(List<OpportunityLineItem> oppLines : linesByOpp){
List<PricebookEntry> pricebookEntries = entriesByOpp[oppIdx];
Integer lineIdx = 0;
for(OpportunityLineItem oppLine : oppLines){
oppLine.OpportunityId = opps[oppIdx].Id;
oppLine.PricebookEntryId = pricebookEntries[lineIdx++].Id;
}
allLines.addAll(oppLines);
oppIdx++;
}
insert allLines;
The unit of work knows what order to insert things in because it's handed a list of SObjectTypes in dependency order when it's constructed. Typically you define this list exactly once for your whole org (and as you'll see in the fflib_Application chapter, that's precisely what the Application class does for you):
//SObjects, in order of dependency
private static List<Schema.SObjectType> MY_SOBJECTS =
new Schema.SObjectType[]{
Product2.SObjectType,
PricebookEntry.SObjectType,
Opportunity.SObjectType,
OpportunityLineItem.SObjectType };
Why you can't just rely on Apex rolling back for you
"But wait," I hear you say, "Salesforce already rolls back my transaction when something fails. Why do I need a savepoint at all?" Here's where it gets weird: the platform's automatic rollback only happens when the request fails - and the moment you write a try/catch, you're preventing the request from failing.
Look at this method. It has a deliberate bug in the middle, and a perfectly reasonable-looking catch block that returns a friendly error to the Lightning component that called it:
@AuraEnabled
public static String doSomeWork(){
try{
Opportunity opp = new Opportunity();
opp.Name = 'My New Opportunity';
opp.StageName = 'Open';
opp.CloseDate = System.today();
insert opp;
Product2 product = new Product2();
product.Name = 'My New Product';
insert product;
PricebookEntry pbe = new PricebookEntry();
pbe.UnitPrice = 10;
pbe.IsActive = true;
pbe.UseStandardPrice = false;
pbe.Pricebook2Id = [SELECT Id FROM Pricebook2 WHERE IsStandard = true].Id;
pbe.Product2Id = product.Id;
insert pbe;
//Something goes wrong
Integer x = 42 / 0;
//This insert never runs
OpportunityLineItem oppLineItem = new OpportunityLineItem();
oppLineItem.Quantity = 1;
oppLineItem.TotalPrice = 10;
oppLineItem.PricebookEntryId = pbe.Id;
insert oppLineItem;
return 'Success!';
}
catch(Exception e){
//Returning a friendly error message instead of blowing up...
//...seems considerate, right?
return 'Something went wrong: ' + e.getMessage();
}
}
Per Salesforce's own transaction control documentation, changes are only rolled back "if the request does not complete successfully". By catching the exception and returning normally, this method makes the request complete successfully - so the Apex runtime happily commits everything that ran before the error. The user gets a polite error message, and the database gets an Opportunity with a Product and PricebookEntry but no line items. Congratulations, your good error-handling manners just corrupted your data.
The fix is a Database.setSavepoint() before the work starts and a Database.rollback() in the error path - which every developer has to remember, in every method, forever. Or you use a unit of work: commitWork() sets a savepoint before it touches the database, rolls back to it if anything fails, and then re-throws the exception so your calling code can still do its own error handling and reporting. You get consistent transactional behavior no matter how the calling code handles its errors.
Whether it's commitWork() or your own savepoint logic doing the rolling back, the Ids already stamped onto your in-memory sObject variables are not cleared by a rollback. Don't try to re-insert those same object instances afterward - the runtime will reject records that already have an Id.
The honest cost
It's only fair to acknowledge the trade: a generic library doing your DML is never going to be quite as optimal as code you hand-tuned for one specific scenario. commitWork() has its own processing overhead, and in extremely high-volume contexts (large Batch Apex jobs, say) that overhead is worth measuring before you commit to it.
But statement count is just one thing on the scale. Weigh it against query count, DML count, transactional consistency and overall code complexity - because smaller, simpler codebases generally contain fewer bugs, and the maintenance cost of a thousand hand-rolled stitching loops is very real. For the vast majority of business logic, the unit of work wins that trade easily. Just make the decision on balance rather than on reflex.