Platform Events vs Change Data Capture — Use Cases & Trade-offs (Salesforce)

Share

Salesforce offers two major ways to stream events across systems — Platform Events (PE) and Change Data Capture (CDC).
Both use the Salesforce Event Bus with replay support, but they exist for different reasons.

In short:

  • Platform Events are custom, intentional messages you define to represent business actions or commands.

  • Change Data Capture is automatic change tracking — Salesforce emits these events when your data changes.

Your choice depends on whether you want to convey business intent or track record-level data changes.


What They Are (and Aren’t)

Platform Events

You define your own schema and publish events intentionally — via Apex, Flow, Process Builder, or APIs.

They’re perfect for domain-level events like “InvoicePaid” or commands like “GenerateQuote.”
They help you coordinate workflows across multiple systems without tying the logic to specific objects.

Platform Events are independent of standard sObjects, and their payloads can include correlation IDs, multiple record details, or contextual data.
They’re published within the transaction but are only delivered after the transaction successfully commits — meaning no commit, no delivery.


Change Data Capture (CDC)

CDC automatically generates ChangeEvent messages whenever records change on enabled objects.

It’s great for data synchronization, audit logging, cache invalidation, or streaming updates to data lakes and warehouses.

Each CDC event carries detailed metadata — which fields changed, before-and-after values, transaction context, and commit information.
It’s object-specific and schema-controlled by Salesforce.


Selection Matrix

Dimension Platform Events (PE) Change Data Capture (CDC)
Primary intent Business/domain signal or command Ground truth of what changed on a record
Schema control Custom (you design fields) Fixed ChangeEvent schema (fields + metadata)
Who emits? Your app (Apex, Flow, API) Salesforce platform (on DML commit)
When emitted When you publish On every create, update, delete, undelete
Tied to sObject Optional Always (one stream per object)
Typical consumers Microservices, downstream workflows, external workers ETL/ELT, search, analytics, caches
Filtering By subscriber logic or payload intent Object/field enablement; header selectors
Ordering At-least-once; no global ordering Per-transaction; includes metadata
Idempotency You handle it (keys, dedup table) Replay duplicates possible; use Tx metadata
Replay Yes (retention window) Yes (retention window)
Best for Domain orchestration, business commands Reliable replication, change deltas

Quick rule of thumb:
If you can describe it as a business event — “PaymentCaptured,” “OrderShipped,” “UserVerified” — use Platform Events.
If it’s more like a data change — “Account.Name changed from X to Y” — use Change Data Capture.


Real-World Example (Step-by-Step)

Let’s imagine a sales process:
When an Opportunity is marked Closed Won, you need to:

  1. Send a domain-level event (RevenueBooked) to trigger billing and fulfillment — via Platform Events.

  2. Stream data changes for Account records into a data lake — via Change Data Capture.

  3. Keep both sides idempotent and traceable.


A) Platform Event — Publishing a Domain Intent (“RevenueBooked”)

Define a Platform Event called Revenue_Booked__e with fields like:

  • OpportunityId__c (Text)

  • Amount__c (Number)

  • Currency__c (Text)

  • MessageId__c (Text UUID)

  • CorrelationId__c (Text)

  • DedupKey__c (Text) — for example: OppId:ClosedWon:v1

Publish this event from an after-update trigger or handler on Opportunity:

public with sharing class RevenueBookedPublisher {
    public static void publishFor(List<Opportunity> opps) {
        List<Revenue_Booked__e> events = new List<Revenue_Booked__e>();
        for (Opportunity o : opps) {
            Revenue_Booked__e e = new Revenue_Booked__e(
                OpportunityId__c = (String)o.Id,
                Amount__c        = o.Amount,
                Currency__c      = o.CurrencyIsoCode,
                MessageId__c     = Crypto.getRandomUUID(),
                CorrelationId__c = String.valueOf(o.Id),
                DedupKey__c      = String.valueOf(o.Id) + ':ClosedWon:v1'
            );
            events.add(e);
        }
        Database.SaveResult[] sr = EventBus.publish(events);
        // Optionally inspect sr for failures (rare unless validation limits hit)
    }
}

Tiny trigger delegating to handler

trigger OpportunityTrigger on Opportunity (after update) {
    List<Opportunity> newlyWon = new List<Opportunity>();
    for (Opportunity n : Trigger.new) {
        Opportunity o = Trigger.oldMap.get(n.Id);
        if (n.StageName == 'Closed Won' && o.StageName != 'Closed Won') newlyWon.add(n);
    }
    if (!newlyWon.isEmpty()) RevenueBookedPublisher.publishFor(newlyWon);
}

Why Platform Events here?
Because you’re signaling a business milestone — a meaningful event that other systems (billing, finance, fulfillment) can respond to asynchronously. It’s not just about data; it’s about intent.


B) Change Data Capture — Reacting to Account Changes in Apex

When CDC is enabled for an object (e.g., Account), Salesforce automatically fires ChangeEvent records like AccountChangeEvent.
You can handle them directly in Apex using ChangeEvent triggers or subscribe to them externally using CometD or the Pub/Sub API.

Apex ChangeEvent trigger example:

// Fires when Account changes (create/update/delete/undelete)
// Enable CDC for Account in Setup first.
trigger AccountChangeEventTrigger on AccountChangeEvent (after insert) {
    // Minimal example: queue a worker to sync downstream
    List<Id> changedAccountIds = new List<Id>();
    for (AccountChangeEvent e : Trigger.new) {
        // Header fields include ChangeEventHeader with changedFields, commitUser, transactionKey, etc.
        if (e.ChangeEventHeader.changeType == 'UPDATE') {
            changedAccountIds.add((Id)e.ChangeEventHeader.recordIds[0]);
        }
    }
    if (!changedAccountIds.isEmpty()) System.enqueueJob(new AccountSyncJob(changedAccountIds));
}

Queueable consumer reading the final committed state

public with sharing class AccountSyncJob implements Queueable {
    private List<Id> ids;
    public AccountSyncJob(List<Id> ids) { this.ids = ids; }

    public void execute(QueueableContext qc) {
        // Rehydrate final committed state
        List<Account> accs = [SELECT Id, Name, Industry, LastModifiedDate FROM Account WHERE Id IN :ids];

        // Upsert to your external system (pseudo)
        // Http callout / middleware call goes here (consider Database.AllowsCallouts)
        System.debug('Syncing ' + accs.size() + ' accounts.');
    }
}

Why CDC here?
You’re not signaling intent — you just want every Account change with precise field-level context for downstream synchronization or analytics


C) Idempotency & Correlation for Both Streams

Both event types benefit from idempotent consumers — ensuring actions are processed only once.

PE Consumer Pattern:

// Idempotency__c(Key__c unique)
// In the PE consumer trigger:
Boolean firstTime = Idempotency.claim(pe.DedupKey__c);
if (firstTime) {
   // apply side effects; use pe.CorrelationId__c in logs/records
}

CDC Consumer Dedup Pattern:

Use a composite key made of:
ChangeEventHeader.transactionKey + recordIds[0] + sequenceNumber
Store this in a small log or custom table, and skip processing if it’s already been seen.

Platform Events vs Change Data


Final Thoughts

Use Platform Events when you need to communicate intent, coordinate processes, or trigger workflows across applications.

Use Change Data Capture when you need a reliable stream of actual record changes for replication, caching, or analytics.

In many architectures, you’ll want both — PEs for milestones and CDC for state synchronization.
Design consumers to be idempotent, use CorrelationIds for traceability, and persist replay checkpoints to build resilience.

  • October 21, 2025