Skip to content

// Architecture Styles · 05

Event-Driven Architecture

Producerleads toBrokerleads toConsumers

Announce what happened; whoever cares reacts. Nobody waits.

How it fits together

Event-Driven Architecture structureOrders serviceorder + outbox, one txOrderPlaced topicmessage brokerBillingidempotent handlerEmailidempotent handlerAnalyticsidempotent handlerDead-letter queuepublishafter N retries
publishes / notifiesfails / error path
  • Orders service publishes / notifies OrderPlaced topic (publish)
  • OrderPlaced topic publishes / notifies Billing
  • OrderPlaced topic publishes / notifies Email
  • OrderPlaced topic publishes / notifies Analytics
  • Email fails / error path Dead-letter queue (after N retries)

Spot it

  • One action calls five services in a row
  • Adding a side effect means editing the caller
  • A slow downstream stalls the whole request

It costs

Loose coupling, but flows are harder to trace and state is eventually consistent.

In code

public sealed record OrderPlaced(Guid EventId, Guid OrderId, decimal Total);​public sealed class PlaceOrderHandler(OrdersDbContext db){    public async Task HandleAsync(Order order, CancellationToken ct)    {        var placed = new OrderPlaced(Guid.NewGuid(), order.Id, order.Total);        db.Orders.Add(order);        // A relay process publishes outbox rows to the broker after commit.        db.Outbox.Add(OutboxMessage.From(placed));        await db.SaveChangesAsync(ct);    }}​public sealed class BillingOrderPlacedHandler(BillingDbContext db){    public async Task HandleAsync(OrderPlaced message, CancellationToken ct)    {        if (await db.ProcessedEvents.AnyAsync(e => e.EventId == message.EventId, ct))            return;​        db.Invoices.Add(Invoice.For(message.OrderId, message.Total));        db.ProcessedEvents.Add(new ProcessedEvent(message.EventId));        await db.SaveChangesAsync(ct);    }}
Go deeperThe full explanation and what to ask for · 1 min readExpand

Producers publish that something happened and do not know who listens. New behaviour is added by adding a subscriber, not by editing the producer, and a slow consumer no longer blocks the caller.

The cost is visibility: the business flow is spread across handlers, so you need correlation IDs and tracing. Brokers (Kafka, RabbitMQ, SQS/SNS, Azure Service Bus) deliver at least once and not always in order, so consumers must cope with both. When the caller needs an answer right away, a direct call is simpler.

An event states a fact; a command asks for something to happen. Event sourcing stores events as the source of truth, which is a separate decision: you can be event-driven without it.

Ask for it

  • domain event
  • integration event
  • publish/subscribe
  • at-least-once delivery
  • idempotent consumer
  • correlation ID

When an order is placed, publish an OrderPlaced integration event to a broker topic through a transactional outbox, instead of calling billing and email directly. Make each subscriber idempotent and give it a dead-letter queue.