Unit of Work Pattern
Intent
The Unit of Work pattern maintains a list of objects affected by a business transaction and coordinates writing out changes and resolving concurrency problems. It ensures all DML operations happen in the correct order and as a single transaction.
Problem Context
In Salesforce, you face challenges with:
Complex parent-child relationships requiring specific insert order
Governor limits on DML statements (150 per transaction)
Transaction management across multiple service calls
Bulk operations and trigger recursion
Maintaining data integrity across related objects
Core Implementation
1. Basic Unit of Work Pattern
// Unit of Work interface
public interface IUnitOfWork {
void registerNew(SObject record);
void registerNew(List<SObject> records);
void registerNew(SObject record, Schema.SObjectField relatedToField, SObject relatedTo);
void registerDirty(SObject record);
void registerDirty(List<SObject> records);
void registerDeleted(SObject record);
void registerDeleted(List<SObject> records);
void commitWork();
}
// Application configuration
public class Application {
// Define DML order for related objects
public static final fflib_Application.UnitOfWorkFactory UnitOfWork =
new fflib_Application.UnitOfWorkFactory(
new List<SObjectType>{
Account.SObjectType, // Parents first
Contact.SObjectType, // Then children
Case.SObjectType,
CaseComment.SObjectType, // Grandchildren last
Task.SObjectType,
Event.SObjectType
}
);
public static IUnitOfWork newUnitOfWork() {
return UnitOfWork.newInstance();
}
}2. Understanding Transaction Boundaries
The Mental Model
Think of Unit of Work as a transaction coordinator. Instead of executing DML immediately, you're building a "change list" that executes atomically at the end.
Key Questions When Using UoW:
What's my transaction boundary? - All related changes that must succeed or fail together
What's the relationship hierarchy? - Parents must exist before children can reference them
Where are my side effects? - Emails, events, or custom work that should happen with the transaction
Service Layer Pattern
The service layer is where business transactions are orchestrated:
Why This Matters:
All or nothing - If contract line creation fails, the quote isn't marked accepted
Single DML context - All operations count as one transaction for limits
Predictable state - Database changes happen at a known point
3. Custom Work with IDoWork
When to Use Custom Work
The registerWork pattern allows you to inject custom logic into the transaction. This is powerful but should be used thoughtfully.
Use IDoWork When:
You need to execute logic after records are committed but within the transaction
You're orchestrating complex multi-step operations
You need to send emails or publish events as part of the transaction
You want to encapsulate reusable transaction logic
Don't Use IDoWork When:
Simple CRUD operations suffice (use registerNew/Dirty/Deleted)
Logic should run regardless of transaction success
You need immediate execution (IDoWork is deferred)
Real-World Pattern: Post-Conversion Actions
From production code, here's how custom work enables clean separation:
Why This Pattern Works:
Separation of concerns - Core logic vs. post-processing
Extensibility - Easy to add new post-conversion actions
Transaction safety - Actions roll back if they fail
Testability - Mock the work without executing it
4. Email Work Pattern
The Challenge with Transactional Emails
Sending emails within transactions presents unique challenges:
Emails shouldn't send if the transaction rolls back
You want to batch email operations for efficiency
Email templates need record IDs that don't exist until after insert
Solution: Email Work Registration
From production implementations, here's the pattern for transactional emails:
Key Benefits:
Transaction safety - Emails only send if DML succeeds
Bulk efficiency - All emails sent in one call
Template support - WhatId references exist when emails send
Reusability - Same pattern works for any email scenario
Understanding Execution Order
The Unit of Work executes operations in this sequence:
DML operations (in configured SObject order)
Registered work (IDoWork implementations)
Email dispatch (if configured)
Event publishing (if configured)
This matters because:
Your custom work can reference saved record IDs
Emails can use merge fields from committed records
Events publish after data is persisted
Everything rolls back together on failure
5. Testing with Mocked Unit of Work
Why Mock the Unit of Work?
Mocking UoW in tests provides several benefits:
Faster tests - No actual DML operations
Focused testing - Test logic, not database operations
Predictable behavior - No side effects or triggers
Verify interactions - Ensure correct methods are called
The Mocking Pattern
From production test suites, here's the standard approach:
Testing Custom Work
When testing IDoWork implementations:
Testing Strategy:
Mock for logic testing - Verify service behavior
Real UoW for integration - Test actual database operations
Isolate custom work - Test IDoWork implementations directly
Verify error handling - Ensure graceful failure
Implementation Patterns
1. Nested Unit of Work
2. Conditional Registration
Benefits
Transaction Management: All-or-nothing commits
Bulk Operations: Automatic bulkification of DML
Relationship Management: Handles parent-child relationships
Governor Limit Optimization: Minimizes DML statements
Testability: Easy to mock for unit tests
Separation of Concerns: Business logic separate from persistence
Trade-offs
Memory Usage: Holds records in memory until commit
Complexity: Additional abstraction layer
Debugging: Can be harder to trace DML operations
Learning Curve: Team needs to understand pattern
Best Practices
1. Define Clear Object Order
2. Single Unit of Work Per Transaction
3. Pass Unit of Work to Methods
Anti-Patterns to Avoid
1. Premature Commits
2. Mixing Direct DML
Real-World Usage
Production systems use Unit of Work for:
Complex order processing with multiple related objects
Data migration maintaining relationships
Trigger operations managing related records
Integration responses updating multiple objects
Bulk operations from batch processes
The pattern is essential for maintaining data integrity while respecting Salesforce governor limits.
Last updated
Was this helpful?