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:

  1. What's my transaction boundary? - All related changes that must succeed or fail together

  2. What's the relationship hierarchy? - Parents must exist before children can reference them

  3. 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:

  1. DML operations (in configured SObject order)

  2. Registered work (IDoWork implementations)

  3. Email dispatch (if configured)

  4. 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:

  1. Mock for logic testing - Verify service behavior

  2. Real UoW for integration - Test actual database operations

  3. Isolate custom work - Test IDoWork implementations directly

  4. 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?