Real-Time Salesforce ↔ SAP Integration with CDC & Platform Events
Keeping Salesforce and SAP S/4 in sync sounds like a connector problem. It's actually an event-design and idempotency problem. Here's the architecture that ran at 99.8% success.
Order-to-Cash spans two systems of record. The CRM owns the deal; the ERP owns the financial truth. The integration's job is to move the right facts in the right direction without creating duplicates, dropping events, or coupling the two systems so tightly that an SAP outage takes down your Salesforce UI. Three primitives carry most of that load: Change Data Capture, Platform Events, and a middleware layer (MuleSoft, Boomi, or Kafka).
Push vs. poll — decide per direction
The first architectural call: for each direction, do you push or poll?
| Need | Mechanism |
|---|---|
| SF data change → notify outside | Change Data Capture (you didn't write extra code; the platform emits it) |
| SF business event → notify outside | Platform Event (you control the schema & when it fires) |
| Outside → SF, low latency | Middleware calls Composite / Bulk API |
| Outside → SF, high volume / batch | Bulk API 2.0 jobs on a schedule |
CDC vs. Platform Events is the question people get wrong. Use CDC when the consumer cares about field-level data changes on a standard/custom object — it's free and automatic. Use a Platform Event when you want to publish a business fact ("QuoteApproved", "OrderShipped") with a schema you own, decoupled from how the data happens to be stored.
Publishing a business event
Define the event as a first-class contract. Downstream doesn't need your data model — it needs the fact.
// Platform Event: Order_Sync__e
public static void publishOrderSync(List<Order> orders) {
List<Order_Sync__e> events = new List<Order_Sync__e>();
for (Order o : orders) {
events.add(new Order_Sync__e(
Order_Number__c = o.OrderNumber,
Account_Ext_Id__c = o.Account.SAP_Id__c,
Total__c = o.TotalAmount,
Event_Uuid__c = generateUuid() // <-- idempotency key
));
}
List<Database.SaveResult> rs = EventBus.publish(events);
// inspect rs for partial failures and retry
}
Idempotency is the whole game
Networks retry. Middleware redelivers. SAP will, eventually, receive the same event twice. If "create order" isn't idempotent you'll book double revenue. Two defenses, used together:
- A UUID on every event (
Event_Uuid__cabove). The consumer records processed UUIDs and ignores repeats. - Upsert on an external Id rather than insert. When SAP pushes back into Salesforce, target an
External_Id__cfield withupsertso a redelivery updates instead of duplicating.
// Inbound from SAP: upsert on the external key, never blind insert
upsert incomingRecords External_Id__c;
If you remember one thing: design every integration message to be safely processable twice. At volume, "exactly once" is a fantasy — "effectively once via idempotency" is what actually ships.
Why a middleware layer at all
You can point Salesforce straight at SAP. You shouldn't. A middleware tier (MuleSoft / Boomi / Kafka) buys you:
- Decoupling — SAP downtime queues messages instead of throwing errors into your Salesforce transaction.
- Transformation — the canonical-model mapping between two very different schemas lives in one place, not smeared across Apex.
- Buffering & replay — Kafka's log lets you replay a bad window after a fix, which is gold during an incident.
- Throttling — protect SAP from a 100K-record Salesforce bulk load by rate-limiting at the middleware.
The CDC + Kafka pipeline
A pattern I've shipped: Salesforce emits CDC, middleware subscribes to the CDC channel, normalizes to a canonical message, and produces onto a Kafka topic that SAP-side consumers read at their own pace. The inverse flows SAP → Kafka → middleware → Salesforce Bulk/Composite API.
Salesforce ──CDC──▶ Middleware ──produce──▶ Kafka topic ──▶ SAP consumer
SAP system ──────▶ Kafka topic ──consume──▶ Middleware ──Bulk API──▶ Salesforce
The Kafka log in the middle is what turns "we lost three hours of orders" into "we replayed the topic from offset X." That replayability is most of why the success rate stays at 99.8% instead of 96%.
Operational must-haves
- A dead-letter channel. Messages that fail N retries go somewhere visible, not into the void.
- Correlation IDs end to end. One id you can grep across Salesforce, middleware, Kafka, and SAP logs.
- A reconciliation job. Nightly count/checksum comparison catches the silent drift that real-time sync never quite eliminates.
- Backpressure awareness. Know what happens when Salesforce emits faster than SAP can consume — and make sure the answer is "queue," not "drop."
The connector is the easy 20%. The event design, idempotency, and operability are the 80% that decides whether the integration is trusted or quietly feared.