Using Salesforce Platform Events to Decouple Order Processing
Learn how to publish a Platform Event from an Order insert and build an idempotent subscriber that keeps integration logic separate from core Salesforce transactions.
13 Feb 2026, 20:22 UTC

The problem: tight coupling hurts reliability
When a sales representative creates an Order in Salesforce, downstream systems (like an external fulfillment service) often need to know about the new record immediately. A common approach is to make a synchronous callout from an Apex trigger after the Order insert. This works until the external service is slow or returns an error: the entire Salesforce transaction rolls back, the user sees an error, and the Order may never be saved. Moreover, the trigger now carries integration concerns, making it harder to test and evolve.
The goal is to keep the Order creation transaction fast and independent of any external system while still ensuring that interested parties eventually receive the information.
How Platform Events give you loose coupling
Platform Events are custom event objects that live on the Salesforce Event Bus. When you publish an event, Salesforce writes it to the bus only after the publishing transaction commits. Subscribers—either a Platform Event Trigger in Apex or an external client via CometD or Streaming API—receive the event in a separate transaction. Key characteristics:
- At‑least‑once delivery: the bus will retry until it believes the event was delivered, so duplicates can appear.
- Replay ID: each event gets a numeric identifier that lets a subscriber resume from a known point after a downtime.
- No guaranteed ordering across different publishing transactions: events from two separate Order inserts may arrive in any order.
Because the publish and subscribe happen in different transactions, a failure in the subscriber does not roll back the Order save, and the core logic stays free of integration code.
Worked example: publishing OrderCreated__e and processing it idempotently
1. Define the Platform Event
In Setup → Platform Events → New Platform Event, create a custom event called OrderCreated__e. Add these fields:
OrderId__c(Text, 18 chars) – the Salesforce Order record Id.OrderNumber__c(Text) – the Order’s human‑readable number.Amount__c(Currency) – total amount.
Save the event; Salesforce creates the API name OrderCreated__e.
2. Publish the event from an Order trigger
Create an after‑insert trigger on the Order object (requires "Author Apex" permission). The trigger runs in the same transaction as the Order insert but only publishes after the commit.
trigger OrderAfterInsert on Order (after insert) {
List events = new List();
for (Order o : Trigger.New) {
OrderCreated__e ev = new OrderCreated__e();
ev.OrderId__c = o.Id;
ev.OrderNumber__c = o.OrderNumber;
ev.Amount__c = o.TotalAmount;
events.add(ev);
}
if (!events.isEmpty()) {
EventBus.publish(events);
}
}
Where to run: Save the trigger in the Developer Console or via VS Code with Salesforce DX. No special permissions beyond "Author Apex" and "Modify All Data" (if needed for the Order object).
Expected check: After inserting an Order via the UI or Data Loader, open Setup → Monitoring → Event Monitoring → Platform Events and look for a published OrderCreated__e entry. The publish succeeds only if the Order insert committed.
3. Build an idempotent subscriber with a Platform Event Trigger
Create a Platform Event Trigger that processes OrderCreated__e. This trigger runs in its own transaction, separate from the Order save.
trigger OrderCreatedHandler on OrderCreated__e (after insert) {
Set orderIds = new Set();
for (OrderCreated__e e : Trigger.New) {
orderIds.add((Id)e.OrderId__c);
}
// Query any Orders that have already been marked as processed
Map processed = new Map([
SELECT Id, PlatformEventProcessed__c
FROM Order
WHERE Id IN :orderIds
]);
List toUpdate = new List();
for (OrderCreated__e e : Trigger.New) {
Id ordId = (Id)e.OrderId__c;
Order ord = processed.get(ordId);
if (ord == null) {
// Order not yet fetched; query fresh
ord = [SELECT Id, PlatformEventProcessed__c FROM Order WHERE Id = :ordId];
processed.put(ordId, ord);
}
// Idempotency check: skip if already processed
if (!ord.PlatformEventProcessed__c) {
ord.PlatformEventProcessed__c = true;
// TODO: callout to external system, update custom logs, etc.
toUpdate.add(ord);
}
}
if (!toUpdate.isEmpty()) {
update toUpdate;
}
}
Where to run: Same as the Order trigger; requires "Author Apex" and "Modify All Data" on Order. The custom checkbox PlatformEventProcessed__c (add it to Order) stores the processed flag.
Expected check: After the Order insert, open the Order record and verify that PlatformEventProcessed__c is true. Check the trigger’s debug logs (Setup → Debug Logs) to see that the handler ran once. If you deliberately cause a failure in the handler (e.g., throw an exception after the update), the Order remains saved and the event will be retried; the flag prevents duplicate processing on retry.
Trade‑offs and limits to consider
- Volume limits: Each edition has a daily limit for published Platform Events (e.g., 50,000 events per 24 h for Enterprise). Exceeding this blocks further publishes until the next day. Monitor via Setup → System Overview → Platform Events.
- Payload size: Individual events are limited to 1 MB. Keep fields small; avoid attaching large blobs.
- Retention: Events are retained for 24 hours (or 72 hours with the Event Bus add‑on). Design subscribers to catch up within that window or persist the replay ID elsewhere.
- At‑least‑once delivery: Duplicates are possible; idempotency (as shown) is mandatory.
- No ordering guarantee across transactions: If your downstream system depends on strict sequence, you must add sequencing logic (e.g., using a numeric version field).
Practical way to verify limits: Run a test that publishes 100 events via Anonymous Apex (EventBus.publish(listOfEvents)) and then check the Platform Events usage metric in Setup. If you approach the limit, consider batching or archiving older events.
Actionable closing
Platform Events give you a reliable, decoupled way to propagate changes from Salesforce to external systems without blocking user transactions. Start small:
- Add a Platform Event object that mirrors the data you need to send.
- Publish from an after‑insert/update/delete trigger (or Flow) using
EventBus.publish. - Create a Platform Event Trigger subscriber that checks a deterministic marker (record Id + replay ID or a custom flag) to ensure idempotent processing.
- Monitor usage, set up alerts for nearing limits, and validate replay‑ID handling in a sandbox before moving to production.
By keeping the publish and subscribe in separate transactions, you protect the core Salesforce experience while still delivering timely information to downstream integrations.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.