As our applications grow, we often run into a common, frustrating wall: the Big Ball of Mud.
You know the feeling. You’re working on a simple feature - let’s say, processing an order - and suddenly you find yourself writing code for inventory updates, email notifications, and loyalty point calculations all inside a single transaction. It’s hard to test, impossible to scale, and a nightmare to maintain.
If you’ve been looking for a way to break free from this tight coupling and start building truly distributed, scalable systems, the answer lies in Domain Events.
In our on-demand webinar on domain events, I show how you can implement this pattern effectively using Domain-Driven Design (DDD) principles.
Why Domain Events?
At its core, a domain event is simple: it’s a record of something that happened in your domain that other parts of the system need to know about.
Instead of your “Order” service calling the “Email” service directly, the Order service simply screams into the void: “An order was placed!”
This shift in thinking allows you to:
- Decouple your logic: Your core business entities don’t need to know who is listening to them.
- Improve Reliability: By using patterns like the Outbox Pattern, you can ensure that side effects happen even if a specific service is temporarily down.
- Scale Independently: Different teams can subscribe to events without ever touching the original codebase.
What We’re Covering
In the video, I don’t just talk about the theory; I get into the IDE and show you the implementatio using eShopOnWeb. We walk through:
- Identifying events with EventStorming: This is helpful when you’ve got a system and aren’t sure where to start with domain events. Remember - the orange sticky notes are written in past tense and answer “What happened?”.
- Defining the Event: Creating a clean, immutable representation of a state change.
- Dispatching with MediatR: Using a mediator to handle the “In-Process” side effects seamlessly.
Using Domain Events in Practice
Domain events are simple POCOs that carry data from the domain to handlers. Here’s the OrderCreatedEvent:
public class OrderCreatedEvent(Order order) : DomainEventBase
{
public Order Order { get; init; } = order;
}
Here’s how we use it:
flowchart TD
A[User completes checkout] --> B[OrderService.CreateOrderAsync]
B --> C[Create Order from Basket]
C --> D[Add the order to the database]
D --> E[Create OrderCreatedEvent]
E --> F[Publish the event]
F --> G[OrderCreatedHandler is triggered when it sees the OrderCreatedEvent event]
G --> H[Send confirmation email]
Publishing the Event
Events are published from your service layer after the business operation completes:
public async Task CreateOrderAsync(int basketId, Address shippingAddress)
{
var basketSpec = new BasketWithItemsSpecification(basketId);
var basket = await _basketRepository.FirstOrDefaultAsync(basketSpec);
Guard.Against.Null(basket, nameof(basket));
Guard.Against.EmptyBasketOnCheckout(basket.Items);
var catalogItemsSpecification = new CatalogItemsSpecification(basket.Items.Select(item => item.CatalogItemId).ToArray());
var catalogItems = await _itemRepository.ListAsync(catalogItemsSpecification);
var items = basket.Items.Select(basketItem =>
{
var catalogItem = catalogItems.First(c => c.Id == basketItem.CatalogItemId);
var itemOrdered = new CatalogItemOrdered(catalogItem.Id, catalogItem.Name, _uriComposer.ComposePicUri(catalogItem.PictureUri));
var orderItem = new OrderItem(itemOrdered, basketItem.UnitPrice, basketItem.Quantity);
return orderItem;
}).ToList();
var order = new Order(basket.BuyerId, shippingAddress, items);
await _orderRepository.AddAsync(order);
OrderCreatedEvent orderCreatedEvent = new OrderCreatedEvent(order);
await _mediator.Publish(orderCreatedEvent);
}
Important: The event is published after persistence. This ensures the order exists in the database before any side effects (like sending emails) execute.
Handling the Event
Handlers react to events without the domain knowing anything about them:
public class OrderCreatedHandler(ILogger<OrderCreatedHandler> logger, IEmailSender emailSender) : INotificationHandler<OrderCreatedEvent>
{
public async Task Handle(OrderCreatedEvent domainEvent, CancellationToken cancellationToken)
{
logger.LogInformation("Order #{orderId} placed: ", domainEvent.Order.Id);
await emailSender.SendEmailAsync("to@test.com",
"Order Created",
$"Order with id {domainEvent.Order.Id} was created.");
}
}
In Summary…
Domain events provide a clean way to decouple your domain logic from side effects like emails, notifications, and integrations. In this example:
- The domain creates and persists the Order without knowing anything about emails.
- The event signals that something meaningful happened.
- The handler reacts to that event independently.
This pattern makes it easy to:
- Add new handlers without modifying existing code
- Test the domain logic in isolation from handlers
- Reason about what happens when an order is created
In a real application, you might add handlers to:
- Send confirmation emails to customers
- Notify the fulfillment system
- Update analytics
- Trigger inventory checks
Each handler lives in its own class and can be developed, tested, and deployed independently.
Get started with Domain Events TODAY!
Whether you are a seasoned architect or a developer just starting to explore DDD, understanding how to communicate between different parts of your system via events is a superpower.
Check out the full breakdown here:
If you want to receive an email with a link to this email and supporting links seen in this webinar, sign up today!
I’d love to hear how you’re handling communication in your own systems. Are you using a system like NServiceBus or keeping things in-process for now? Let’s discuss it in the comments of the video!

