Event-Driven Architecture (EDA) is a style where components publish facts such as OrderPlaced, PaymentFailed, or InventoryReserved, and consumers react without the producer naming them directly. It reduces temporal coupling when reactions can happen asynchronously. Event-driven components commonly coexist with synchronous APIs for queries and decisions that need an immediate answer.

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.

Durable cross-process EDA usually uses messaging or a retained log, but a broker is not definitional. In-process event dispatch, database change streams, and HTTP webhooks can also carry events with different durability and coupling contracts.

Core Concepts

Event Types

Domain Event

  • Describes something meaningful that happened inside a bounded context.
  • Produced by domain logic because business state changed.
  • Examples: InvoiceIssued, OrderConfirmed, CustomerUpgradedToPremium.
  • Scope: primarily internal to the service/domain, though some may later be promoted externally.

Integration Event

  • A stable, explicit contract published for other services to consume.
  • Usually emitted after local transaction success and often via an outbox/publisher pipeline.
  • Examples: OrderPlacedIntegrationEvent, PaymentCapturedIntegrationEvent.
  • Scope: cross-service communication. Versioning and backward compatibility matter.

Event Notification

  • Lightweight signal saying “something changed”, often with minimal payload (ID + timestamp + type).
  • Consumers fetch full state separately when needed.
  • Example: CatalogItemChanged { ItemId, ChangedAt }.
  • Scope: low payload fan-out scenarios, cache invalidation, or trigger-based processing.

Difference at a Glance

TypePrimary purposePayload styleTypical audience
Domain EventCapture domain factRich domain dataSame bounded context
Integration EventCross-service contractStable DTO contractOther services
Event NotificationSignal change happenedMinimal metadataMany 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

  • Choreography: looser coupling and easier service autonomy, but harder to trace global flow and reason about emergent behavior as subscriptions grow.
  • Orchestration: clearer process control, easier audit/debug per workflow instance, but introduces a central dependency that can become a bottleneck or single point of operational complexity.

.NET messaging boundary

A broker can deliver an integration event only after the producer places it on the transport. Saving business state and publishing in two independent operations leaves a failure gap: the database can commit while the publish fails. A transactional outbox stores the business change and outgoing message through the same local DbContext transaction, then a delivery service forwards it to the broker.

public sealed record OrderPlacedIntegrationEvent(
    Guid EventId,
    Guid OrderId,
    Guid CustomerId,
    decimal Total,
    DateTime OccurredAtUtc);
builder.Services.AddMassTransit(bus =>
{
    bus.AddEntityFrameworkOutbox<OrdersDbContext>(outbox =>
    {
        outbox.UsePostgres();
        outbox.UseBusOutbox();
    });
 
    bus.UsingRabbitMq((context, rabbit) =>
        rabbit.ConfigureEndpoints(context));
});

With UseBusOutbox, a scoped IPublishEndpoint captures the event in OrdersDbContext; one SaveChangesAsync commits the order and outbox row together. Broker delivery happens afterward and can retry without losing the event.

Consumers face the inverse gap: a handler can commit its business change and crash before acknowledging the message. Configure the Entity Framework consumer outbox so inbox state and the consumer’s changes share a local transaction. Put a unique constraint on the business idempotency key as well, such as one PaymentIntent per OrderId; inbox state suppresses redelivery of one message identity, while the domain constraint protects the invariant if the same fact arrives under another identity.

bus.AddConsumer<OrderPlacedConsumer>();
bus.AddEntityFrameworkOutbox<BillingDbContext>(outbox =>
    outbox.UsePostgres());
 
rabbit.ReceiveEndpoint("billing-order-placed", endpoint =>
{
    endpoint.UseEntityFrameworkOutbox<BillingDbContext>(context);
    endpoint.ConfigureConsumer<OrderPlacedConsumer>(context);
});
FailureDurable stateRecovery
Process stops before producer saveNeither order nor message committedClient may retry with an idempotency key
Process stops after producer saveOrder and outbox row committedOutbox delivery service publishes later
Broker redelivers after consumer commitConsumer change and inbox state committedDuplicate delivery is suppressed
Consumer permanently rejects schema or dataMessage remains unprocessedDead-letter with alert and replay procedure

The outbox closes a local database-to-broker gap. It does not turn the broker and every downstream database into one global exactly-once transaction.

Governance and data pipelines

The governance visual is one organization-specific topology. The reusable boundary is centralized compatibility and telemetry guardrails with domain-owned event meaning:

  • Registry: schema versions, owner, compatibility mode, lifecycle, and data classification.
  • SDK: a narrow paved road for envelopes, trace context, serialization, and telemetry without hiding broker semantics.
  • Gateway: optional ingress for authentication, quotas, and routing; internal producers do not all need an extra hop.
  • Domain ownership: producers own event meaning and availability; the platform owns guardrails and shared infrastructure.
  • Regional isolation: replication declares lag, ordering, conflict, residency, and failover behavior.

For MenuItemPriceChanged, the Restaurant domain owns semantics and its producer SLO. CI checks the schema against the registry. Regional brokers keep local consumers running during a remote outage; a global consumer accepts delayed and duplicate replicated events. Event Schema Evolution covers compatibility across retained messages and independently deployed consumers.

The pipeline visual names conceptual stages; real batch and streaming paths can combine or skip them. Trace checkout-42 through the reusable stages:

  1. Collect: checkout emits an event ID, trace ID, schema ID, tenant, and event time.
  2. Ingest: the broker assigns partition and offset and exposes lag.
  3. Store: object storage writes immutable raw records partitioned by event date and schema version.
  4. Compute: a stateful job checkpoints offsets and derives DailyRevenue; malformed records enter an owned quarantine path.
  5. Consume: warehouse and alerting outputs declare separate freshness and correctness SLOs.

Preserve source event IDs in derived records and publish lineage from input dataset through job to output. Low broker lag does not prove a warehouse table is fresh or correct. “Exactly once” must name a boundary: a stream processor may atomically checkpoint input offsets and write one managed sink, while an external email or payment call remains at-least-once and needs idempotency.

Pitfalls

1) Event Ordering

  • What goes wrong: consumers may process OrderCancelled before OrderPlaced (or receive updates in different order across partitions/queues).
  • Why: distributed brokers and parallel consumers do not guarantee global ordering.
  • Mitigation: design handlers for per-aggregate ordering where needed (partition by aggregate key), include version/sequence in events, and detect stale events.

2) Idempotency

  • What goes wrong: duplicate delivery causes duplicate side effects (double charge, duplicate email, repeated inventory decrement).
  • Why: at-least-once delivery is common in real systems.
  • Mitigation: use deterministic idempotency keys (EventId), store processed-message fingerprints, and make state transitions conditional.

3) Event Schema Evolution

  • What goes wrong: a producer ships a breaking payload change and multiple consumers fail.
  • Why: integration events are shared contracts with independent deployment cycles.
  • Mitigation: version events, evolve contracts backward-compatibly (additive first), and validate in contract tests before release.

4) Distributed Flow Debugging

  • What goes wrong: incidents are hard to reconstruct across many async hops.
  • Why: no single request thread shows full workflow.
  • Mitigation: propagate correlation/causation IDs, instrument with OpenTelemetry traces/metrics/logs, and keep searchable event audit logs.

Questions

References

ByteByteGo provenance