Event-Driven Architecture
Event-Driven Architecture (EDA) is a design pattern where components communicate by producing and consuming events rather than calling each other directly.
Core Concepts
Section titled โCore Conceptsโ| Concept | Description |
|---|---|
| Event | A record of something that happened (past tense) |
| Producer | The component that emits the event |
| Consumer | The component that reacts to the event |
| Event Broker | The infrastructure that routes events (Kafka, RabbitMQ, Azure Service Bus) |
Producer โโโบ [ Event Broker ] โโโบ Consumer A โโโบ Consumer B โโโบ Consumer CEvent Types
Section titled โEvent TypesโDomain Event โ something meaningful happened in the domain:
public record OrderPlaced(Guid OrderId, Guid CustomerId, decimal Total, DateTime OccurredAt);public record PaymentReceived(Guid PaymentId, Guid OrderId, decimal Amount);public record ItemShipped(Guid OrderId, string TrackingNumber);Integration Event โ crosses service boundaries (published to a message broker):
public record UserRegisteredIntegrationEvent(Guid UserId, string Email, DateTime RegisteredAt);In-Process Events (Domain Events)
Section titled โIn-Process Events (Domain Events)โUseful within a single application, no broker needed:
// Define a simple dispatcherpublic interface IDomainEventDispatcher{ Task DispatchAsync<T>(T domainEvent) where T : IDomainEvent;}
// Raise events inside domain entitiespublic class Order{ private readonly List<IDomainEvent> _events = new(); public IReadOnlyList<IDomainEvent> DomainEvents => _events;
public void Place() { Status = OrderStatus.Placed; _events.Add(new OrderPlaced(Id, CustomerId, Total, DateTime.UtcNow)); }}
// Dispatch after saving to DBpublic class PlaceOrderHandler{ public async Task HandleAsync(PlaceOrderCommand cmd) { var order = new Order(cmd.CustomerId, cmd.Lines); order.Place(); await _repo.SaveAsync(order);
foreach (var e in order.DomainEvents) await _dispatcher.DispatchAsync(e); }}Out-of-Process Events with RabbitMQ
Section titled โOut-of-Process Events with RabbitMQโdotnet add package MassTransit.RabbitMQProducer:
public class OrderService{ private readonly IPublishEndpoint _bus;
public OrderService(IPublishEndpoint bus) => _bus = bus;
public async Task PlaceOrderAsync(PlaceOrderDto dto) { // ... save order to DB ... await _bus.Publish(new OrderPlacedEvent(order.Id, order.CustomerId, order.Total)); }}Consumer:
public class SendConfirmationEmailConsumer : IConsumer<OrderPlacedEvent>{ private readonly IEmailService _email;
public SendConfirmationEmailConsumer(IEmailService email) => _email = email;
public async Task Consume(ConsumeContext<OrderPlacedEvent> context) { var e = context.Message; await _email.SendOrderConfirmationAsync(e.CustomerId, e.OrderId); }}Registration:
builder.Services.AddMassTransit(x =>{ x.AddConsumer<SendConfirmationEmailConsumer>(); x.UsingRabbitMq((ctx, cfg) => { cfg.Host("rabbitmq://localhost"); cfg.ConfigureEndpoints(ctx); });});Azure Service Bus Example
Section titled โAzure Service Bus Exampleโ// Send a messageawait serviceBusClient.CreateSender("orders").SendMessageAsync( new ServiceBusMessage(JsonSerializer.Serialize(new OrderPlacedEvent(order.Id))));
// Receive and processvar processor = serviceBusClient.CreateProcessor("orders");processor.ProcessMessageAsync += async args =>{ var e = JsonSerializer.Deserialize<OrderPlacedEvent>(args.Message.Body); await HandleOrderPlaced(e); await args.CompleteMessageAsync(args.Message);};Outbox Pattern (Guaranteed Delivery)
Section titled โOutbox Pattern (Guaranteed Delivery)โPrevents events from being lost if the broker is down when the DB transaction commits:
// Save event to outbox table in the same DB transactionusing var tx = await _db.BeginTransactionAsync();await _db.Orders.AddAsync(order);await _db.OutboxMessages.AddAsync(new OutboxMessage{ Id = Guid.NewGuid(), Type = nameof(OrderPlacedEvent), Payload = JsonSerializer.Serialize(new OrderPlacedEvent(order.Id)), CreatedAt = DateTime.UtcNow});await _db.SaveChangesAsync();await tx.CommitAsync();
// Background worker reads outbox and publishes to brokerEDA vs Request-Response
Section titled โEDA vs Request-Responseโ| Aspect | Request-Response | Event-Driven |
|---|---|---|
| Coupling | Tight (caller knows callee) | Loose (producer doesnโt know consumers) |
| Availability | Both sides must be up | Consumer can be down, processes later |
| Consistency | Synchronous, immediate | Eventually consistent |
| Complexity | Simple | Higher (broker, error handling, ordering) |
| Scalability | Limited by callee capacity | Consumers scale independently |
When to Use EDA
Section titled โWhen to Use EDAโGood fit for:
- Decoupling microservices
- Workflows that trigger multiple downstream actions
- Audit trails and event sourcing
- Long-running processes (order fulfillment, billing)
Avoid when:
- You need an immediate synchronous response
- Simple CRUD with no downstream side effects
- Team is small and the operational overhead isnโt justified