An ERP system is usually the system of record for orders, inventory, and financials, and increasingly the thing everyone wants events from — a warehouse system that needs to know the moment a sales order is confirmed, a notification service that wants to fire the second an invoice posts. Wiring each of those consumers directly into the ERP's API turns into a point-to-point mess fast. Putting Kafka in the middle as an event bus is the standard fix, but the ERP side of that integration has a few sharp edges specific to how ERP systems store and expose their data.
Getting events out: CDC vs. application-level publishing
There are two honest ways to get ERP changes onto Kafka. Change Data Capture (Debezium against the ERP's database, for example) tails the transaction log and publishes a row-level event for every insert, update, and delete — no ERP code changes needed, but you get raw table-shape events that leak internal schema details and can fire multiple times for a single logical business action (an order confirmation might touch five tables). Application-level publishing — hooking into the ERP's business logic or workflow/event framework to emit a purpose-built "OrderConfirmed" event — gives you a clean, stable event shape that maps to a real business action, but requires customization work inside the ERP and only fires for changes that go through that customized code path, missing anything done via direct API or database access.
A common pattern is CDC for broad, low-effort visibility into changes (useful for audit and analytics consumers who can tolerate raw table events), and application-level publishing for the specific, well-defined business events that downstream systems actually build logic against. Don't force one approach to cover both use cases.
The outbox pattern for reliable publishing
If you're publishing from application code — a customization that fires after an order is saved — the naive approach (save the record, then publish to Kafka) has a gap: if the process crashes between the database commit and the Kafka publish, the event is silently lost, and nothing downstream ever finds out the order was confirmed. The outbox pattern closes that gap by writing the event to an "outbox" table in the same database transaction as the business data change, then a separate poller (or CDC connector) reads the outbox table and publishes to Kafka. This guarantees the event is published if and only if the business change committed, because they're the same transaction.
BEGIN;
UPDATE sales_orders
SET status = 'confirmed', confirmed_at = NOW()
WHERE order_id = @OrderId;
INSERT INTO outbox_events (aggregate_id, event_type, payload, created_at)
VALUES (
@OrderId,
'OrderConfirmed',
jsonb_build_object('order_id', @OrderId, 'confirmed_at', NOW()),
NOW()
);
COMMIT;
-- A Debezium connector on outbox_events (or a polling publisher)
-- picks up the new row and publishes it to Kafka.
Consuming back into the ERP: idempotency is non-negotiable
The reverse direction — an external system publishing events that need to update the ERP, like a warehouse system confirming a shipment — is where most integration bugs live. Kafka consumers get redelivered messages after a rebalance or a consumer restart, so the ERP-side handler needs to be idempotent: processing the same "ShipmentConfirmed" event twice must produce the same end state as processing it once, not double-decrement inventory or double-post a transaction. The standard fix is tracking processed event IDs (or a natural idempotency key from the event payload) and short-circuiting if you've already applied that exact event.
Many ERP "create record" API calls will happily create a duplicate if called twice with the same logical data but no explicit dedup key. Before wiring a Kafka consumer to call an ERP API on every message, confirm whether the API supports an idempotency key or external reference field — if not, you need to check for an existing record with that reference before creating a new one.
Schema stability matters more here than in most Kafka use cases
ERP customizations change over budget cycles and version upgrades, and every consumer of an ERP-sourced topic is a system you may not control end to end. Version your event schemas explicitly (a schema registry with backward-compatible evolution rules, or at minimum an explicit version field in the payload) so that an ERP customization change doesn't silently break every downstream consumer the day it ships. This matters more here than in a pure microservices context because ERP release cycles and integration consumer release cycles are rarely coordinated by the same team.
Wrapping up
Kafka-ERP integration comes down to three decisions: how you get events out (CDC for broad low-effort visibility, application-level publishing via the outbox pattern for clean business events), how consumers guard against redelivery (idempotency keys, not hope), and how you keep the event schema stable across ERP customization changes that you don't fully control. Get those three right and the ERP becomes a genuine event source instead of a system everyone polls.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.