Event-Driven Architecture: Queues, Streams, and the Outbox

Event-driven architecture decouples producers from consumers and enables asynchronous work, high throughput, and clean audit trails.

Event-Driven Architecture: When It Pays Off and When It Bites

Event-driven architecture (EDA) is the pattern in which system components communicate primarily by producing and consuming events on a message bus rather than by direct synchronous calls. Well-applied it unlocks throughput, resilience, and clean team boundaries. Naively applied it produces a distributed monolith with worse debuggability than the synchronous version it replaced. The interesting questions are which parts of the system belong on the bus and which do not.

Queues, streams, and pub/sub — pick the right primitive

Queue (SQS, RabbitMQ) — one producer, one consumer per message; delete after processing. Best for task offloading (send email, resize image). Stream (Kafka, Kinesis, Pulsar, Redpanda) — durable ordered log; multiple independent consumers can replay from any offset. Best for analytics fanout, event sourcing, cross-team integration. Pub/sub (SNS, Google Pub/Sub) — fire-and-forget broadcast. Best for notifications and loose coupling between services. Choose by ordering, replay, and consumer count requirements — not by fashion. Mixing them per use case is normal and correct.

The outbox pattern for reliable publishing

The classic bug: you commit a row to Postgres and publish an event to Kafka, one succeeds and one fails, state and events drift. The outbox pattern fixes this: in the same DB transaction that changes state, INSERT a row into an outbox table. A separate publisher process reads from outbox and publishes to the bus, marking rows sent. If the publisher crashes, unsent rows are retried on restart. Guarantees at-least-once delivery correlated with state changes, without distributed transactions. Every EDA system that cannot afford lost events should implement outbox (or use a change-data-capture tool like Debezium that reads the DB log and publishes automatically).

Idempotency and duplicate handling

Assume every event will be delivered more than once (queue redelivery, publisher retry after crash, consumer replay after bug fix). Design consumers to be idempotent: include a stable event_id in every event; consumer stores processed event_ids in an idempotency store; skip re-processing. For side effects (charge a card, send an email), rely on downstream idempotency keys (Stripe supports them; email providers have deduplication windows). Never assume exactly-once delivery — the true 'exactly-once' guarantees you'll find in vendor marketing depend on strict conditions that break under real failure modes.

Schema evolution: the long-term killer

Events published today will be consumed by services you have not written yet. Breaking an event schema breaks unknown downstream consumers you cannot easily coordinate with. Rules: (1) Schema registry (Confluent Schema Registry, AWS Glue, Buf Schema Registry) — every event conforms to a versioned schema, compatibility checks in CI. (2) Additive changes only — add optional fields; never rename, never change types. (3) Deprecation cycle — mark fields deprecated, wait 90 days minimum, remove in a new schema version. (4) Publish schema changes in a changelog. Investment here pays off compounded — an EDA without schema discipline collapses under its own weight in 18 months.

Debuggability: the hidden cost

A synchronous request has a stack trace. An event-driven flow has an event that produced another event that produced a state change three services later. Investment required: (1) trace_id propagated through every event and correlated in traces (OpenTelemetry, Datadog APM, Honeycomb). (2) Event lineage tooling — a UI that shows, for one event_id, every consumer that processed it and every event it produced. (3) A 'reprocess from this point' operation for recovering after a bug fix. (4) Dead-letter queues with alerts and a UI to inspect and retry. Do not adopt EDA without budgeting for this tooling — the cost of debugging without it is measured in outages.

Frequently asked questions

When should we adopt EDA?
When you have specific coupling problems synchronous calls create: cross-team dependencies causing deploy coordination, throughput bottlenecks from serialized work, needs for replay or historical fanout. Adopting EDA for its own sake in a monolith is a common early-mistake — the coordination cost usually outweighs the coupling relief until 3-5 teams are stepping on each other.
Kafka or a managed alternative?
For most Series A/B startups, managed alternatives (Redpanda Cloud, Confluent Cloud, AWS MSK Serverless, Google Pub/Sub) are the right default. Self-managed Kafka is powerful and operationally expensive; run it yourself only when scale or cost economics justify a dedicated platform team.
How do events fit with GraphQL / REST APIs?
APIs remain the request/response surface for user-driven actions; events power the async side effects and downstream fanout. A user hits POST /orders (API); the handler writes to the DB and outbox; downstream (inventory, fulfillment, notifications) consumes the event. Not either/or.

Related fundraising guides (40)

Investor directory · Fundraising library · Articles A–Z · Company funding database