Async-First Pattern with Platform Events (DoWork)
Overview
Purpose
The DoWork pattern provides a clean abstraction for moving synchronous trigger operations to asynchronous execution using Platform Events. This solves the common challenge of triggers that need to perform operations that would either hit governor limits, cause record locking issues, or need to run after the initial transaction commits.
Context
In complex Salesforce implementations, triggers often need to:
Update the same record that triggered them (after auto-number generation)
Perform callouts to external systems
Execute operations that exceed governor limits when bulkified
Avoid holding database locks during long-running operations
Platform Events provide immediate publication (before transaction commit) with separate execution context, making them ideal for async processing.
Problem Statement
The Challenge
Trigger operations that modify the triggering record or perform complex operations face several challenges:
Record Lock Contention: Direct updates during
afterInsertcauseUNABLE_TO_LOCK_ROWerrors in high-concurrency scenariosGovernor Limits: Complex calculations or callouts may exceed limits when processing bulk records
Auto-Number Timing: Auto-number fields are not populated until after insert, requiring a separate update
Transaction Rollback Risk: Long-running operations increase the risk of entire transaction failure
Why Traditional Approaches Fall Short
@future methods: Cannot accept SObject parameters, limited to 50 calls per transaction
Queueable Apex: Better than @future but still counts against limits and has delay
Batch Apex: Too heavyweight for simple trigger-initiated operations
Direct DML in trigger: Causes record locking and transaction coupling
Solution
Core Concept
The DoWork pattern uses Platform Events as a lightweight message bus. Work items serialize themselves, publish as events, and a trigger deserializes and executes them in a separate transaction. This provides immediate asynchronous execution with automatic retry capabilities.
Implementation Strategy
1. Define the Work Interface
The interface defines the contract that all async work items must fulfill:
2. Create the Abstract Base Class
The abstract class handles serialization and Platform Event publication:
3. Platform Event Trigger Handler
The trigger deserializes and executes work items with retry logic:
Key Components
IDoWork
Interface
Defines the contract for async work items
DoWorkAbstract
Abstract Base
Handles serialization and event publication
DoWork__e
Platform Event
Carries serialized work data between transactions
DoWorkTrigger
Event Handler
Deserializes and executes work items with retry
Implementation Details
Required Setup
Create Platform Event:
DoWork__ewith fields:Work__c(Long Text Area, 131072 chars) - Serialized work itemClassName__c(Text, 255) - Fully qualified class nameRetries__c(Number) - Remaining retry attempts
Deploy Abstract Classes:
IDoWorkinterface andDoWorkAbstractclassCreate Trigger:
DoWorkTriggeronDoWork__e
Code Structure - Concrete Worker Example
Preventing Duplicate Scheduling
When working with triggers, prevent the same async job from being scheduled multiple times in the same transaction:
Configuration Requirements
Platform Event:
DoWork__emust be created with appropriate field limitsPermissions: Users/contexts publishing events need "Publish" permission on
DoWork__eDependencies: Logger utility for error tracking (optional but recommended)
Best Practices
Do's
Use
FOR UPDATEin queries when updating records to prevent concurrent modification issuesDisable triggers when performing DML to prevent recursion
Include meaningful class names for debugging and monitoring
Keep work items as small as possible to stay under the 131KB serialization limit
Use static tracking sets to prevent duplicate scheduling within transactions
Implement
onExceptionfor critical workflows that need failure notification
Don'ts
Do not serialize large object graphs (query fresh data in
doWork())Do not rely on execution order - Platform Events may be processed out of order
Do not store sensitive data in the event payload (it's visible in Event Monitoring)
Do not use for operations that must complete synchronously with the user action
Considerations
Governor Limits
Platform Events have their own limits separate from the triggering transaction
Event payload is limited to 1MB total, but individual Long Text fields max at 131,072 characters
Maximum 250,000 platform event allocations per 24 hours (varies by edition)
Performance Impact
Platform Events are highly performant for async processing
Events are published immediately, not at transaction commit
Parallel processing of events provides horizontal scalability
Security Implications
Event payload is stored temporarily and visible in Event Monitoring
Use appropriate sharing settings (
with sharingvswithout sharing) in work classesConsider field-level security when updating records
Variations
Variation 1: Base Worker with Common State
For domain-specific workers, create an intermediate abstract class:
Variation 2: Worker with Retry Configuration
For operations that may fail transiently:
Testing Approach
Unit Test Strategy
Test Scenarios
Happy Path: Verify work executes successfully and updates records
Retry Logic: Verify retries occur on transient failures
Error Handling: Verify
onExceptionis called when retries are exhaustedDuplicate Prevention: Verify same work item is not scheduled twice
Trade-offs
Immediate trigger completion
Eventual consistency (not real-time)
Separate governor limit context
Added complexity in testing
Built-in retry mechanism
Requires Platform Event monitoring
Prevents record locking
Cannot return values to caller
Horizontal scalability
Event ordering not guaranteed
Last updated
Was this helpful?