Event-Driven Architecture

Intro

Event-Driven Architecture (EDA) is a style where services communicate by publishing and consuming events instead of calling each other directly through synchronous APIs. The core idea is that a producer emits a fact (OrderPlaced, PaymentFailed, InventoryReserved) and does not need to know who reacts to it. This matters because it reduces runtime coupling, allows services to scale independently, and improves resilience when one downstream component is temporarily unavailable. You reach for EDA when workflows cross service boundaries, when processing can be asynchronous, and when you specifically need durable retained events for audit or replay.

In interview terms: EDA is not "just using a queue". It is a contract-driven communication model where events represent state changes, subscribers own their reaction logic, and consistency is typically eventual rather than immediate.

For related foundations, connect this page with Message Queues, RabbitMQ, and Kafka.

Core Concepts

Event Types

Domain Event

Integration Event

Event Notification

Difference at a Glance

Type Primary purpose Payload style Typical audience
Domain Event Capture domain fact Rich domain data Same bounded context
Integration Event Cross-service contract Stable DTO contract Other services
Event Notification Signal change happened Minimal metadata Many listeners that re-query

Practical rule: model domain events first, then map only the externally relevant subset into integration events.

Patterns

Choreography

In choreography, each service reacts to events independently. No central coordinator tells services what to do next.

flowchart LR
    O[Order Service] -->|OrderPlaced| B[Broker]
    B --> P[Payment Service]
    B --> I[Inventory Service]
    P -->|PaymentSucceeded or PaymentFailed| B
    I -->|InventoryReserved or InventoryRejected| B
    B --> N[Notification Service]

Use when teams want autonomy and workflows can be decomposed into independent reactions.

Orchestration

In orchestration, a central component (process manager/saga orchestrator) directs the workflow and issues commands.

sequenceDiagram
    participant OR as Order API
    participant OC as Order Orchestrator
    participant PA as Payment Service
    participant IN as Inventory Service
    participant NO as Notification Service

    OR->>OC: Start checkout orderId
    OC->>PA: Charge payment
    PA-->>OC: PaymentSucceeded
    OC->>IN: Reserve inventory
    IN-->>OC: InventoryReserved
    OC->>NO: Send confirmation

Use when workflow visibility, explicit state handling, and compensation logic are first-class requirements.

Tradeoffs

This example publishes an integration event when an order is placed, then consumes it in a separate service. The publisher does not know who subscribes.

Shared Contract

namespace Contracts;

public record OrderPlacedIntegrationEvent(
    Guid EventId,
    Guid OrderId,
    Guid CustomerId,
    decimal Total,
    DateTime OccurredAtUtc
);

Publisher (Order Service)

using Contracts;
using MassTransit;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddMassTransit(x =>
{
    x.UsingRabbitMq((context, cfg) =>
    {
        cfg.Host("localhost", "/", h =>
        {
            h.Username("guest");
            h.Password("guest");
        });
    });
});

var app = builder.Build();

app.MapPost("/orders", async (IPublishEndpoint publish) =>
{
    var orderId = Guid.NewGuid();

    // Imagine local DB transaction succeeded before this publish.
    var evt = new OrderPlacedIntegrationEvent(
        EventId: Guid.NewGuid(),
        OrderId: orderId,
        CustomerId: Guid.NewGuid(),
        Total: 129.90m,
        OccurredAtUtc: DateTime.UtcNow
    );

    await publish.Publish(evt);
    return Results.Accepted($"/orders/{orderId}", new { orderId });
});

app.Run();

Consumer (Billing Service)

using Contracts;
using MassTransit;

public sealed class OrderPlacedConsumer : IConsumer<OrderPlacedIntegrationEvent>
{
    public async Task Consume(ConsumeContext<OrderPlacedIntegrationEvent> context)
    {
        var message = context.Message;

        // Idempotency key: message.EventId (store processed IDs in durable storage).
        // Business action: create payment intent, emit PaymentRequested event, etc.
        await Task.CompletedTask;
    }
}
using MassTransit;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddMassTransit(x =>
{
    x.AddConsumer<OrderPlacedConsumer>();

    x.UsingRabbitMq((context, cfg) =>
    {
        cfg.Host("localhost", "/", h =>
        {
            h.Username("guest");
            h.Password("guest");
        });

        cfg.ReceiveEndpoint("billing-order-placed", e =>
        {
            e.ConfigureConsumer<OrderPlacedConsumer>(context);
        });
    });
});

var app = builder.Build();
app.Run();

Production note: pair publish with the transactional outbox pattern to avoid "DB commit succeeded but event publish failed" gaps.

Pitfalls

1) Event Ordering

2) Idempotency

3) Event Schema Evolution

4) Distributed Flow Debugging

Questions

References


Whats next