Most ERP integrations start as point-to-point calls: when an order ships, call the CRM's API, then the invoicing system's API, then the warehouse system's API, in a row, inline, hoping none of them time out. That works fine for the first two integrations. By the fifth, the ERP is a switchboard operator, and every new downstream system means editing code that has nothing to do with it. Pub/sub is the fix for that specific pain — not a general upgrade, a fix for one problem: too many systems needing to know about the same event.
What pub/sub buys you over direct calls
With publish/subscribe, the ERP doesn't call anyone. It publishes one event — order.created, say — to a broker, and whatever is listening (a fulfillment service, a CRM sync job, an analytics pipeline) picks it up independently. The ERP doesn't know or care who's listening, doesn't wait for a response, and doesn't fail if a downstream system is down for maintenance. Adding a sixth consumer is a subscription, not a code change in the publisher. The tradeoff is that you lose the immediate, synchronous "did this succeed" feedback a direct call gives you — you trade it for decoupling and durability.
Designing topics around domain events, not tables
The temptation is to publish a raw "SalesOrder row changed" event because it's easy to wire up from a database trigger or hook. Resist it. A generic change event forces every consumer to inspect the payload and guess what actually happened, which turns your event schema into an implicit, undocumented state machine. Model events as things that happened in the business: order.created, order.line_added, inventory.adjusted, invoice.posted. Each is a fact, past tense, with a payload that carries what changed — not the whole row, just enough for a consumer to act or decide to fetch more.
{
"event_id": "b6b0b4b0-6e2e-4b7a-9c1f-2f1a9b3d9e11",
"event_type": "order.created",
"occurred_at": "2026-07-28T09:14:03Z",
"source": "erp.sales",
"data": {
"order_id": "SO-10482",
"customer_id": "C-2291",
"total": 4820.00,
"currency": "KES"
}
}
The event_id is not decoration — it's what makes the next section possible.
Delivery guarantees: at-least-once is the default, so plan for duplicates
Almost every practical broker — SNS/SQS, Kafka, RabbitMQ with acks, Azure Service Bus — gives you at-least-once delivery, not exactly-once. A consumer can crash after processing a message but before acknowledging it, and the broker will redeliver. Exactly-once delivery across a network is a much harder guarantee to buy than most teams realize, and most brokers that advertise it are really doing at-least-once delivery plus deduplication on the consumer side. So build the deduplication yourself: keep a small table of processed event_ids (with a TTL if you want to bound it), and check-and-insert before acting on an event.
Don't wait for a broker feature to solve duplicate delivery. A unique constraint on event_id in your processing table, checked in the same transaction as the side effect, is enough. If the insert fails because the event was already processed, skip the work and ack the message.
When pub/sub is overkill
If you have one ERP and one downstream system, a webhook is simpler, easier to debug, and gives you a synchronous response you can act on immediately. Pub/sub earns its complexity when there are three or more independent consumers of the same event, when consumers need to be able to go offline without losing events, or when you want to add new consumers without touching the publisher. Standing up a broker, topic naming conventions, and dead-letter handling for a single webhook relationship is solving a problem you don't have yet.
Once a consumer starts failing on a message — a bad payload, a downstream outage — you need somewhere for it to land after N retries, and an alert when it does. Without a DLQ, failed messages either retry forever or silently vanish, and both are worse than the direct-call failure mode you were trying to avoid.
Wrapping up
Pub/sub replaces "who do I need to call" with "what happened," and that shift is worth it exactly when you have multiple independent consumers of the same ERP event. It costs you a broker to operate, a schema to version, and idempotent consumers to write. If you only have one consumer, skip it and use a webhook — you can always add the broker later when a second consumer shows up.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.