Async-First Pattern with Platform Events (DoWork)

Overview

circle-info

When to use this pattern: When trigger operations need to run asynchronously to avoid governor limits, prevent record locking, or ensure immediate trigger completion while deferring complex processing.

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:

  1. Record Lock Contention: Direct updates during afterInsert cause UNABLE_TO_LOCK_ROW errors in high-concurrency scenarios

  2. Governor Limits: Complex calculations or callouts may exceed limits when processing bulk records

  3. Auto-Number Timing: Auto-number fields are not populated until after insert, requiring a separate update

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

Component
Purpose
Responsibility

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

  1. Create Platform Event: DoWork__e with fields:

    • Work__c (Long Text Area, 131072 chars) - Serialized work item

    • ClassName__c (Text, 255) - Fully qualified class name

    • Retries__c (Number) - Remaining retry attempts

  2. Deploy Abstract Classes: IDoWork interface and DoWorkAbstract class

  3. Create Trigger: DoWorkTrigger on DoWork__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__e must be created with appropriate field limits

  • Permissions: Users/contexts publishing events need "Publish" permission on DoWork__e

  • Dependencies: Logger utility for error tracking (optional but recommended)

Best Practices

Do's

  • Use FOR UPDATE in queries when updating records to prevent concurrent modification issues

  • Disable 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 onException for 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 sharing vs without sharing) in work classes

  • Consider 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

  1. Happy Path: Verify work executes successfully and updates records

  2. Retry Logic: Verify retries occur on transient failures

  3. Error Handling: Verify onException is called when retries are exhausted

  4. Duplicate Prevention: Verify same work item is not scheduled twice

Trade-offs

Benefit
Cost

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?