Skip to content

AfterUpdate.Dispatcher

Collects the records that qualify and makes one bulk call per chunk with them: enqueue a Queueable, publish events or send email.

Interface

apex
public class AfterUpdate {
    public interface Dispatcher extends Handler {
        Boolean dispatchOnAfterUpdateWhen(TriggerHandler.UpdateRecord record);
        void dispatchOnAfterUpdate(TriggerHandler.UpdateRecords records);
    }
}
  • dispatchOnAfterUpdateWhen(record): called once per record in the chunk; it only selects records; a record that has used up its recursion budget is skipped without a call. Return true to include the record in the dispatch.
  • dispatchOnAfterUpdate(records): called once, after every record was checked, with the qualified records; not called when none qualified.

Example

apex
public with sharing class ContactDispatcher implements AfterUpdate.Dispatcher {
    public Boolean dispatchOnAfterUpdateWhen(TriggerHandler.UpdateRecord record) {
        return record.isChanged(Contact.Email);
    }

    public void dispatchOnAfterUpdate(TriggerHandler.UpdateRecords records) {
        System.enqueueJob(new ContactSyncJob(records.getIds()));
    }

    public class ContactSyncJob implements Queueable {
        private Set<Id> recordIds;

        public ContactSyncJob(Set<Id> recordIds) {
            this.recordIds = recordIds;
        }

        public void execute(QueueableContext context) {
        }
    }
}
apex
public with sharing class OpportunityWonSyncDispatcher implements AfterUpdate.Dispatcher {
    public Boolean dispatchOnAfterUpdateWhen(TriggerHandler.UpdateRecord record) {
        return record.isChangedTo(Opportunity.StageName, 'Closed Won');
    }

    public void dispatchOnAfterUpdate(TriggerHandler.UpdateRecords records) {
        System.enqueueJob(new SyncJob(records.getIds()));
    }

    public class SyncJob implements Queueable, Database.AllowsCallouts {
        private Set<Id> opportunityIds;

        public SyncJob(Set<Id> opportunityIds) {
            this.opportunityIds = opportunityIds;
        }

        public void execute(QueueableContext context) {
            HttpRequest request = new HttpRequest();
            request.setEndpoint('callout:Erp/opportunities');
            request.setMethod('POST');
            request.setBody(JSON.serialize([SELECT Id, Name, Amount FROM Opportunity WHERE Id IN :this.opportunityIds]));
            new Http().send(request);
        }
    }
}

Good to Know

  • One dispatch per chunk. An update of 1,000 records dispatches 5 times, so the example enqueues 5 jobs. A synchronous transaction can enqueue at most 50 Queueable jobs.
  • No callouts from the trigger. A callout here throws System.CalloutException. Call out from a Queueable that implements Database.AllowsCallouts, and pass it records.getIds().
  • No unit of work. DML here runs at once and is not merged with the Writers' changes. Use a Writer for record changes that commit with the save.
  • Writers' changes may not be saved yet. By default, their registrations commit after the last handler, so a query here does not see them.
  • Writer wins. A class that also implements AfterUpdate.Writer runs only as a Writer. Its Dispatcher methods never run.

Test

apex
@IsTest
static void dispatchOnAfterUpdateWhenStageChangedToClosedWon() {
    // Setup
    TriggerHandler.UpdateRecord record = new TriggerHandler.TriggerRecord(new Opportunity(StageName = 'Closed Won'), new Opportunity(StageName = 'Negotiation/Review'));

    // Test
    Boolean result = new OpportunityWonSyncDispatcher().dispatchOnAfterUpdateWhen(record);

    // Verify
    Assert.isTrue(result, 'The record should qualify.');
}