SaaS · Events

Event Bus Architecture for SaaS

Event Bus Architecture for SaaS is the work that turns a collection of business systems into a coherent operation.

John Kihiu12 min read

An event bus decouples the systems that produce facts from the systems that react to them. Instead of Service A calling Service B directly and waiting on the response, A publishes "order placed" and walks away; B, C, and D subscribe to that event and do their own work on their own schedule. The appeal is obvious — new consumers can be added without touching the producer — but the bus itself introduces a new set of failure modes that request/response never had. This is a tour of the decisions that actually matter when you put a broker in the middle of your architecture.

Pub/sub versus point-to-point

"Event bus" gets used loosely, so it's worth separating two shapes. A point-to-point queue (classic SQS, or RabbitMQ with a single consumer group) delivers each message to exactly one consumer — it's a work queue, good for distributing tasks. Pub/sub (SNS fan-out, Kafka topics with multiple consumer groups, RabbitMQ fanout/topic exchanges) delivers each event to every interested subscriber independently. Most "event bus" architectures are pub/sub: the producer doesn't know or care who's listening, and the number of subscribers can grow over time without any change to the producer. The trade-off is that pub/sub makes it harder to answer "who consumes this event and why" — that knowledge has to live somewhere other than the producer's code, usually a schema registry or a wiki page that quietly goes stale.

Choosing a broker

Kafka, RabbitMQ, and cloud-native SNS/SQS solve overlapping but different problems. Kafka is a distributed commit log — it retains events for a configured period (not just until they're consumed), supports replay, and scales to very high throughput with ordering guaranteed within a partition. It's the right choice when you need an audit trail, multiple consumers reading the same stream at different speeds, or you're doing event sourcing. RabbitMQ is a traditional message broker — it excels at routing (topic exchanges, priority queues, delayed delivery) and is simpler to run at moderate scale, but once a message is consumed and acked it's gone. SNS+SQS is the low-ops option on AWS: SNS fans a topic out to multiple SQS queues, each queue buffers independently, and you get retry and dead-letter handling largely for free, at the cost of vendor lock-in and less flexible routing than Kafka or RabbitMQ.

Don't reach for Kafka by default

Kafka's operational overhead — partition management, consumer group rebalancing, broker sizing — is real. If you don't need replay, very high throughput, or multiple independent consumer groups reading history, a managed queue like SQS or a simpler broker like RabbitMQ will get you most of the decoupling benefit with a fraction of the ops burden.

Delivery guarantees and why "exactly-once" is mostly a lie

Every broker advertises one of three guarantees: at-most-once (fire and forget, message can be lost), at-least-once (message is redelivered until acked, so it can arrive more than once), or exactly-once (the marketing term). In practice, exactly-once delivery across a network is not achievable in the general case — what brokers like Kafka actually offer is exactly-once processing within their own ecosystem (producer idempotence plus transactional consumers), which is a narrower and more honest claim. For everything else, the realistic default is at-least-once delivery paired with an idempotent consumer: give every event a unique ID, and have the consumer check "have I already processed this ID" before acting on it. That one habit removes most of the pain that duplicate delivery causes.

JSON · EVENT ENVELOPE
{
  "event_id": "3f9a2b7e-8c11-4e4a-9d2f-6b1c0a5e2f88",
  "event_type": "order.placed",
  "occurred_at": "2026-07-23T09:14:02Z",
  "source": "orders-service",
  "schema_version": 2,
  "data": {
    "order_id": "ORD-88213",
    "customer_id": "CUST-4471",
    "total_cents": 24999
  }
}

The event_id is what makes idempotent processing possible; schema_version is what lets you evolve the payload without breaking consumers that haven't upgraded yet.

Backpressure and slow consumers

A queue-based bus absorbs bursts by design — that's half the point of putting a broker between producer and consumer. But absorption isn't infinite. If a consumer falls permanently behind (a downstream database is down, a handler is slow), the queue grows without bound unless something pushes back. The three practical responses are: scale consumers horizontally so throughput matches the producer's rate, apply backpressure by capping in-flight messages per consumer so the broker's own queue depth becomes the buffer, or shed load deliberately with a dead-letter queue for messages that fail repeatedly rather than retrying forever and starving everything behind them. Kafka's consumer lag metric and RabbitMQ's queue depth are the two numbers to alert on — a queue that only grows is a slow-motion outage.

Ordering guarantees

Global ordering across an entire topic is expensive and rarely what you actually need. What you usually need is ordering per entity — all events for order ORD-88213 arrive in the order they were published, even if events for a different order arrive out of order relative to them. Kafka gives you this by partitioning on a key (partition by order_id, and Kafka guarantees order within a partition); SQS FIFO queues do the same with message group IDs. Standard SQS and RabbitMQ without careful configuration give you no ordering guarantee at all, which is fine for independent events and a real bug source if your consumers assume otherwise.

Schema evolution and coupling

An event bus decouples deployment timing, but producer and consumer are still coupled through the event's shape. The failure mode that bites teams a year in is a producer adding a required field, removing a field a consumer relied on, or renaming something — and finding out only when a consumer starts throwing deserialization errors in production. Treat the event schema as a versioned public contract: additive changes (new optional fields) are safe, anything else needs a new schema_version and a deprecation window where both shapes are published. A schema registry (Confluent Schema Registry for Kafka, or even a shared JSON Schema repo) turns this from a tribal-knowledge problem into something CI can enforce.

BrokerBest fitOrderingRetention
KafkaHigh throughput, replay, event sourcingPer-partitionConfigurable, can be indefinite
RabbitMQFlexible routing, moderate scalePer-queue (with care)Until consumed
SNS + SQSLow-ops AWS-native fan-outFIFO queues onlyUntil consumed (max 14 days)

None of this is exotic — the failure modes are well understood and the fixes are boring: idempotent consumers, per-entity partitioning, versioned schemas, and dead-letter queues instead of infinite retries. The mistake worth avoiding is treating the bus as a black box that guarantees correctness on its own. It guarantees delivery semantics; correctness under duplication, reordering, and schema drift is still the application's job.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.