When Salesforce code grows, DML statements can quickly become scattered across triggers, services, and helper classes. The fflib Unit of Work pattern helps by collecting pending database changes and committing them together at the end of a business transaction. This makes code easier to maintain, encourages bulk-safe design, and helps keep related changes inside one managed transaction. The fflib docs also note that Unit of Work is generally created in the Service layer, and the transaction is finalized with commitWork() at the end. [fflib.dev], [deepwiki.com], [fflib.dev]
In fflib, the Unit of Work tracks several categories of record changes before commit: new records for insert, dirty records for update, and deleted records for delete. The framework provides methods such as registerNew, registerDirty, registerDeleted, and commitWork() to manage these changes in a structured way. Documentation and source summaries also highlight that the implementation is designed to bulkify DML, maintain transaction integrity, and apply operations in a consistent sequence. [deepwiki.com], [deepwiki.com], [github.com]
A particularly useful method is registerDirty(...). In the Unit of Work pattern, a dirty record means an existing record that has already been loaded or identified, then modified in memory, and now needs to be updated in the database when commitWork() is called. That gives you a clean way to separate business logic from database execution. [deepwiki.com], [deepwiki.com]
Why use Unit of Work in Apex?
Apex developers often try to keep DML outside loops and avoid mixing persistent operations with business logic. Unit of Work supports this goal by centralizing inserts, updates, and deletes into one place. According to the framework documentation and source summaries, this pattern helps with bulkified DML, atomic transaction handling, and even relationship management for related objects when needed. [deepwiki.com], [deepwiki.com], [github.com]
Another useful detail from the fflib docs is that the order of SObject types passed into the Unit of Work can matter, especially when object dependencies exist. For a simple Account example, that is not a major issue, but it becomes important in multi-object transactions such as Account + Contact or Opportunity + OpportunityLineItem. [fflib.dev]
A simple but proper Apex example
Below is a clean example using a single service class to demonstrate:
- Insert with
registerNew - Update with
registerDirty - Delete with
registerDeleted
This example keeps the pattern simple and readable while still reflecting how Unit of Work is intended to be used in a service-oriented design. The class uses Account records only, which makes the learning path straightforward. The code itself below is original and written for this post. The method choices are based on the documented Unit of Work API. [fflib.dev], [deepwiki.com], [deepwiki.com]
Apex Service Class
public with sharing class AccountUnitOfWorkService {
/** * Inserts new Accounts using Unit of Work. */ public static void createAccounts(List<String> accountNames) { if (accountNames == null || accountNames.isEmpty()) { return; }
fflib_ISObjectUnitOfWork uow = new fflib_SObjectUnitOfWork( new List<SObjectType>{ Account.SObjectType } );
for (String accountName : accountNames) { if (String.isBlank(accountName)) { continue; }
Account acc = new Account( Name = accountName.trim() );
uow.registerNew(acc); }
uow.commitWork(); }
/** * Updates existing Account names using registerDirty. * Map key = Account Id * Map value = New Account Name */ public static void updateAccountNames(Map<Id, String> accountNamesById) { if (accountNamesById == null || accountNamesById.isEmpty()) { return; }
List<Account> accountsToUpdate = [ SELECT Id, Name FROM Account WHERE Id IN :accountNamesById.keySet() ];
fflib_ISObjectUnitOfWork uow = new fflib_SObjectUnitOfWork( new List<SObjectType>{ Account.SObjectType } );
for (Account acc : accountsToUpdate) { String newName = accountNamesById.get(acc.Id);
if (String.isBlank(newName)) { continue; }
String trimmedName = newName.trim(); if (acc.Name != trimmedName) { acc.Name = trimmedName; uow.registerDirty(acc); } }
uow.commitWork(); }
/** * Deletes Accounts using Unit of Work. */ public static void deleteAccounts(Set<Id> accountIds) { if (accountIds == null || accountIds.isEmpty()) { return; }
List<Account> accountsToDelete = [ SELECT Id FROM Account WHERE Id IN :accountIds ];
fflib_ISObjectUnitOfWork uow = new fflib_SObjectUnitOfWork( new List<SObjectType>{ Account.SObjectType } );
for (Account acc : accountsToDelete) { uow.registerDeleted(acc); }
uow.commitWork(); }}
How this class maps to the Unit of Work pattern
The createAccounts method builds new Account records in memory and registers each one with registerNew(...). These records are not inserted immediately; instead, they are staged inside the Unit of Work until commitWork() runs. That aligns with the documented behavior of Unit of Work tracking new records and then inserting them in a single managed commit. [deepwiki.com], [deepwiki.com]
The updateAccountNames method demonstrates the most important concept for many developers: dirty records. First, it queries existing Accounts, then updates the Name field in memory, and finally uses registerDirty(acc) to tell the Unit of Work that the record must be updated when the transaction is committed. This follows the documented purpose of registerDirty(...) for existing modified records. [deepwiki.com], [deepwiki.com]
The deleteAccounts method selects existing records and adds them to the Unit of Work using registerDeleted(...). The actual delete does not happen until commitWork() is called, which matches how the framework tracks deleted records before commit. [deepwiki.com], [deepwiki.com]
The createAccounts method builds new Account records in memory and registers each one with registerNew(...). These records are not inserted immediately; instead, they are staged inside the Unit of Work until commitWork() runs. That aligns with the documented behavior of Unit of Work tracking new records and then inserting them in a single managed commit. [deepwiki.com], [deepwiki.com]
The updateAccountNames method demonstrates the most important concept for many developers: dirty records. First, it queries existing Accounts, then updates the Name field in memory, and finally uses registerDirty(acc) to tell the Unit of Work that the record must be updated when the transaction is committed. This follows the documented purpose of registerDirty(...) for existing modified records. [deepwiki.com], [deepwiki.com]
The deleteAccounts method selects existing records and adds them to the Unit of Work using registerDeleted(...). The actual delete does not happen until commitWork() is called, which matches how the framework tracks deleted records before commit. [deepwiki.com], [deepwiki.com]
Example usage
Here is a simple example of how the service methods could be called from anonymous Apex or another orchestration layer:
// InsertAccountUnitOfWorkService.createAccounts( new List<String>{ 'Acme UK', 'Cloud Nova Ltd', 'Northwind Services' });
// UpdateMap<Id, String> nameUpdates = new Map<Id, String>();nameUpdates.put('001XXXXXXXXXXXXAAA', 'Acme UK - Updated');nameUpdates.put('001YYYYYYYYYYYYAAA', 'Cloud Nova Ltd - Updated');AccountUnitOfWorkService.updateAccountNames(nameUpdates);
// DeleteSet<Id> accountsToDelete = new Set<Id>{ '001XXXXXXXXXXXXAAA', '001YYYYYYYYYYYYAAA'};AccountUnitOfWorkService.deleteAccounts(accountsToDelete);
This style keeps calling code clean and leaves DML orchestration in one dedicated service class, which is consistent with the service-layer guidance in the fflib docs. [fflib.dev], [fflib.dev]
Here is a simple example of how the service methods could be called from anonymous Apex or another orchestration layer:
Optional test class
A blog post is much stronger when it includes a working test. Here is a compact example:
@IsTestprivate class AccountUnitOfWorkServiceTest {
@IsTest static void testInsertUpdateDeleteUsingUnitOfWork() { // Insert Test.startTest(); AccountUnitOfWorkService.createAccounts( new List<String>{ 'Original Account' } ); Test.stopTest();
Account inserted = [ SELECT Id, Name FROM Account WHERE Name = 'Original Account' LIMIT 1 ]; System.assertNotEquals(null, inserted.Id);
// Update using registerDirty Test.startTest(); AccountUnitOfWorkService.updateAccountNames( new Map<Id, String>{ inserted.Id => 'Updated Account' } ); Test.stopTest();
Account updated = [ SELECT Id, Name FROM Account WHERE Id = :inserted.Id ]; System.assertEquals('Updated Account', updated.Name);
// Delete Test.startTest(); AccountUnitOfWorkService.deleteAccounts( new Set<Id>{ inserted.Id } ); Test.stopTest();
Integer remaining = [ SELECT COUNT() FROM Account WHERE Id = :inserted.Id ]; System.assertEquals(0, remaining); }}
This test validates the three major flows in one place: insert, update via registerDirty, and delete. Because Unit of Work collects changes and applies them on commit, it becomes much easier to reason about the transaction boundary in testing as well. [deepwiki.com], [deepwiki.com]
A blog post is much stronger when it includes a working test. Here is a compact example:
This test validates the three major flows in one place: insert, update via registerDirty, and delete. Because Unit of Work collects changes and applies them on commit, it becomes much easier to reason about the transaction boundary in testing as well. [deepwiki.com], [deepwiki.com]
Best practices to mention in your blog
If you publish this article, here are a few practical recommendations worth calling out:
Create the Unit of Work in the service layer and commit once at the end of the business transaction whenever possible. That is consistent with the fflib usage guidance. [fflib.dev], [fflib.dev]
Use registerDirty(...) only for existing records that need an update. For brand-new records, use registerNew(...); for deletes, use registerDeleted(...). [deepwiki.com], [deepwiki.com]
Think in bulk, even for simple examples. One of the key benefits of the pattern is reducing scattered DML and keeping operations bulk-friendly. [deepwiki.com], [github.com]
Be mindful of SObject ordering when multiple object types participate in the same Unit of Work, because the docs note that order can matter for dependency handling. [fflib.dev]
If you publish this article, here are a few practical recommendations worth calling out:
Create the Unit of Work in the service layer and commit once at the end of the business transaction whenever possible. That is consistent with the fflib usage guidance. [fflib.dev], [fflib.dev]
Use
registerDirty(...)only for existing records that need an update. For brand-new records, useregisterNew(...); for deletes, useregisterDeleted(...). [deepwiki.com], [deepwiki.com]Think in bulk, even for simple examples. One of the key benefits of the pattern is reducing scattered DML and keeping operations bulk-friendly. [deepwiki.com], [github.com]
Be mindful of SObject ordering when multiple object types participate in the same Unit of Work, because the docs note that order can matter for dependency handling. [fflib.dev]
Conclusion
The fflib Unit of Work pattern gives Apex developers a cleaner way to manage database operations. Instead of inserting, updating, and deleting records directly throughout your code, you can track all pending changes and commit them together in one controlled place. For updates specifically, registerDirty(...) is the key method to remember: it represents an existing record that has been modified and should be saved during commitWork(). This approach improves readability, supports bulkification, and makes transaction boundaries much easier to understand. [fflib.dev], [deepwiki.com], [deepwiki.com]
The fflib Unit of Work pattern gives Apex developers a cleaner way to manage database operations. Instead of inserting, updating, and deleting records directly throughout your code, you can track all pending changes and commit them together in one controlled place. For updates specifically, registerDirty(...) is the key method to remember: it represents an existing record that has been modified and should be saved during commitWork(). This approach improves readability, supports bulkification, and makes transaction boundaries much easier to understand. [fflib.dev], [deepwiki.com], [deepwiki.com]
Reference
Primary reference used for this article: fflib Unit of Work documentation — https://fflib.dev/docs/unit-of-work [fflib.dev]
Additional supporting references:
- fflib framework overview and service-layer guidance. [fflib.dev]
- Unit of Work summaries and API behavior from framework-aligned documentation and source references. [deepwiki.com], [deepwiki.com], [github.com]
Primary reference used for this article: fflib Unit of Work documentation — https://fflib.dev/docs/unit-of-work [fflib.dev]
Additional supporting references:
- fflib framework overview and service-layer guidance. [fflib.dev]
- Unit of Work summaries and API behavior from framework-aligned documentation and source references. [deepwiki.com], [deepwiki.com], [github.com]