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.

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 Force.com 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 it 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, the SavePoint is used to avoid the caller catching exceptions (perhaps resulting from the second DML statement). That would then result in the Apex runtime committing updates to the opportunity lines (first DML statement), causing a partial update to the database.

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...
    List<OpportunityLineItem> oppLinesToUpdate = new List<OpportunityLineItem>();
    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.

  1. Initialize a single Unit of Work and use it to scope all the work done by the service method.
  2. Have the service layer logic register records with the Unit of Work when it executes.
  3. 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.

How to include Unit of Work: create a new unit of work instance, do work and register record changes and finally commit a bulkified unit of work to the database.

Note

Note

If you’re calling between services, pass the outer Unit of Work instance as a parameter via method overloading. Don’t create a new one. Because the Unit of Work is representing the transactional scope of the service method, aim for only one Unit of Work instance per method call, as shown below.

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_SObectUnitWork 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

retargeting