Learn Unit of Work Principles
Learning Objectives
After completing this unit, you’ll be able to:
- Effectively manage your DML operations and avoid partial database updates.
- Understand the features and benefits of the Apex implementation of the pattern.
- Apply the Unit Of Work pattern to the applyDiscount service method from the previous unit.
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 54:05 minute mark, in case you want to rewind and watch the beginning of the step again.)
Unit of Work Principles
The Unit of Work is a design pattern that reduces repetitive code when implementing transaction management and the coding overheads of adhering to DML bulkification through extensive use of maps and lists. It’s not a requirement for implementing a service layer, but it can help. We're going to show you a before and after example to explain how this works.
The Unit of Work pattern used in this module is based on the pattern described by Martin Fowler: "Maintains a list of objects affected by a business transaction and coordinates the writing out of changes and the resolution of concurrency problems."
On the Salesforce platform this translates to the pattern handling the following use cases:
- Recording record updates, inserts, and deletes to implement a specific business requirement
- Recording record relationships to make inserting child or related records easier with less coding
- When asked to write (or commit) to the database, bulkifies all records captured
- Wrapping DML performed in SavePoint, freeing the developer from implementing this each time for every service method that is written
Implementing a Service Method Without a Unit of Work
To better understand what the Unit of Work pattern has to offer, let’s first review the code we need to write without using the Unit of Work in each service method, while still adhering to the design best practices discussed earlier. We need to write code for a specific business requirement, but also have it serve as boilerplate code to implement the following:
-
DML bulkification and optimization - The code can update some or all of the Opportunity or OpportunityLineItem records, depending on the logic flow. It creates and populates two lists to maintain only the records read that need to be updated.
-
Error handling and transaction management - As per the design principles of the Service layer, it must commit all changes or none if an error occurs, regardless if the caller catches any exceptions it throws. Recall that the platform automatically rolls back only if exceptions are unhandled, which is not desirable from a user-exception perspective. It’s good practice for the Service layer code to manage a transaction scope using the SavePoint facility and the try/catch semantics.
The following example uses a SavePoint
to encapsulate and wrap the database operations within a Service method, as per the design considerations. Why? Imagine a scenario where the second DML operation fails. Without a SavePoint
in the method:
- If the caller doesn’t handle the exception, the entire transaction, including the first DML operation, will be rolled back, as this is the default Apex transactional behavior.
- If the caller catches the exception, without allowing it to propagate or without restoring a SavePoint, the Apex runtime will commit the updates to the opportunity lines (first DML statement), causing a partial update to the database.
When not using the Unit of Work pattern, it is best practice to handle multiple DML operations as demonstrated in this example. However, as you will see in the next sections, the Unit of Work can handle this for you.
public static void applyDiscounts(Set<Id> opportunityIds, Decimal discountPercentage) { // Validate parameters // ... // Query Opportunities and Lines // ... // Update Opportunities and Lines (if present) List<Opportunity> oppsToUpdate = new List<Opportunity>(); List<OpportunityLineItem> oppLinesToUpdate = new List<OpportunityLineItem>(); // Do some work... Decimal factor = 1 - (discountPercentage==null ? 0 : discountPercentage / 100); for(Opportunity opportunity : opportunities) { // Apply to Opportunity Amount if(opportunity.OpportunityLineItems!=null && opportunity.OpportunityLineItems.size()>0) { for(OpportunityLineItem oppLineItem : opportunity.OpportunityLineItems) { oppLineItem.UnitPrice = oppLineItem.UnitPrice * factor; oppLinesToUpdate.add(oppLineItem); } } else { opportunity.Amount = opportunity.Amount * factor; oppsToUpdate.add(opportunity); } } // Update the database SavePoint sp = Database.setSavePoint(); try { update oppLinesToUpdate; update oppsToUpdate; } catch (Exception e) { // Rollback Database.rollback(sp); // Throw exception on to caller throw e; } }
Apex Implementation of the Unit of Work Pattern
The remainder of this unit references an Apex Open-source library that contains an implementation of Martin Fowler’s Unit of Work pattern. It is implemented through a single class, fflib_SObjectUnitOfWork, so go ahead and open this in another tab. In the next unit, we'll get hands-on with this class but for now, let's just understand a little more about some of its key methods.
This class exposes methods to allow an instance of the fflib_SObjectUnitOfWork
class to capture records that need to be created, updated, or deleted as the service code is executed via the register methods. In addition, the commitWork method encapsulates the SavePoint and try/catch convention.
Updating the database with DML occurs only when the commitWork method is called. Therefore, the service code can call the register methods as often and as frequently as needed, even in loops. This approach allows the developer to focus on the business logic and not on code to manage multiple lists and maps.
Finally, as shown in the following diagram, the Unit of Work scope is determined by the start and finish of your service method code. Only call the commitWork once in the scope of the service method.
To include the Unit of Work in your service code methods, follow these steps.
- Initialize a single Unit of Work and use it to scope all the work done by the service method.
- Have the service layer logic register records with the Unit of Work when it executes.
- Call the Unit of Work commitWork method to bulkify and execute the DML.
The following diagram illustrates the above steps and enforces the scope of each step in respect to the service method code execution.
To use the Unit of Work class, you need to construct it with a list of the objects that your code is interacting with. The objects must be in dependency order to ensure that parent and child records registered are inserted by the commitWork method in the correct order. We’ll explore more about fflib_SObjectUnitWork parent-child relationship handling in the next unit.
fflib_SObjectUnitOfWork uow = new fflib_SObjectUnitOfWork( new List<SObjectType> { OpportunityLineItem.SObjectType, Opportunity.SObjectType } );
Implementing a Service Method with the Unit of Work
The following example applies the Unit of Work pattern to the service we created in the previous unit. Code that has not changed is not shown. Notice that the lists are gone and that the SavePoint doesn’t have try/catch boilerplate code around it because this is all handled by the fflib_SObjectUnitOfWork
class.
public static void applyDiscounts(Set<Id> opportunityIds, Decimal discountPercentage) { // Unit of Work fflib_SObjectUnitOfWork uow = new fflib_SObjectUnitOfWork( new List<SObjectType> { OpportunityLineItem.SObjectType, Opportunity.SObjectType } ); // Validate parameters // ... // Query Opportunities and Lines // ... // Update Opportunities and Lines (if present) // ... for(Opportunity opportunity : opportunities) { // Apply to Opportunity Amount if(opportunity.OpportunityLineItems!=null && opportunity.OpportunityLineItems.size()>0) { for(OpportunityLineItem oppLineItem : opportunity.OpportunityLineItems) { oppLineItem.UnitPrice = oppLineItem.UnitPrice * factor; uow.registerDirty(oppLineItem); } } else { opportunity.Amount = opportunity.Amount * factor; uow.registerDirty(opportunity); } } // Commit Unit of Work uow.commitWork(); }
The fflib_SObjectUnitOfWork class aggregates DML operations and wraps them in a SavePoint when the commitWork method is called.
In more complex code, with multiple depths and classes, you can choose to pass SObjectUnitOfWork (or use a static). The called code can continue to register its own database updates, knowing that the owner of the Unit of Work, in this case, the service layer, performs a single commit or rollback phase on its behalf.
Resources
- Apex Enterprise Patterns - GitHub Repo
- Separation of Concerns (Wikipedia)
- Martin Fowler’s Enterprise Architecture Patterns
- Martin Fowler’s Unit of Work Patterns
- Managing your DML and Transactions with a Unit of Work
- Doing more work with the Unit of Work