DEV Community

Ben Witt
Ben Witt

Posted on

Domain Events

Introduction to domain events

Definition of domain events:

Domain events are events that occur in a specific area or domain and are important for the business logic of an application. They represent significant state changes or activities within the system. In contrast to system-wide events, which can affect the entire application, domain events are closely linked to the specific domain or specialist area of your application.

Importance of domain events in software development:

Domain events play a crucial role in the implementation of domain-driven design (DDD), an approach to developing complex software applications that focuses on the domain and its rules. They enable the mapping of real processes and events in the software and support a clearly structured and modularized architecture.

Why are domain events important?

Domain events offer several advantages, including:

  • Decoupling of components:

By using domain events, different parts of your application can work independently of each other as they only communicate via events without having to access each other directly.

  • Traceability and auditing:

Domain Events serve as a log of important events in your application, facilitating traceability of activities and providing the ability to meet auditing and compliance requirements.

  • Increased flexibility and extensibility:

By using Domain Events, your application can be made more flexible and extensible as changes in the domain can be implemented more easily and existing functionality can be modified without affecting other parts of the application.

Advantages of domain events

Improvement in modularity and scalability:

Using domain events improves the modularity of your application, as individual components are loosely coupled and can work independently of each other. This also facilitates scalability, as you can add or remove individual components as required without significantly changing the overall structure of your application.

Decoupling of domain logic and infrastructure:

Domain events support the separation of domain logic and infrastructure by ensuring that your domain objects have no direct dependencies on external systems or frameworks. This leads to a cleaner and more maintainable code base.

Support for domain-driven design (DDD):

Domain events are a core element of domain-driven design and enable you to map the technical language of your domain directly in your software. This promotes a common understanding between developers and domain experts and leads to a better alignment of the software with the real requirements of your domain.

Implementation of domain events

Identification of relevant events in the domain:

Before you can implement domain events, it is important to identify the relevant events in your domain. These events should represent significant state changes or activities within your application that are of interest to other parts of the system.

Creation of event classes and interfaces:

For each domain event identified, you should create a corresponding event class that contains the relevant information about the event. These classes can also implement interfaces to enable consistent handling and processing of events.

Publication and subscription of domain events:

To use domain events in your application, you need to implement mechanisms for publishing and subscribing to events. This can be done, for example, by using event buses or brokers that allow different parts of your application to react to events and act accordingly.

Integration of domain events in applications

Use of event buses or brokers:

To integrate domain events in your application, you can use event buses or brokers. These components enable the central administration and distribution of events to the corresponding recipients within your application. Examples of event busses are RabbitMQ, Kafka or simple in-memory event busses.

Handling of errors and transaction limits:

When integrating domain events, it is important to consider the handling of errors and the consideration of transaction boundaries. You should ensure that events are processed atomically and that no data inconsistencies occur, especially in the case of failed transactions.

Testing strategies for domain events:

For reliable integration of domain events, it is essential to develop appropriate testing strategies. This includes testing event generation, publication and processing as well as testing scenarios with faulty event processing to ensure the robustness of your application.

Practical application of domain events

Example application: e-commerce order processing:

To illustrate the use of domain events in a practical application, let’s look at an example from the field of e-commerce: order processing. In this scenario, various events can occur that can be represented by domain events, e.g:

  • “Order placed”
  • “Order paid”
  • “Order shipped”
  • “Order canceled”

Triggering events for order status changes:

When the status of an order changes, e.g. from “placed” to “paid”, corresponding domain events can be triggered and published. These events can be subscribed to by other parts of the application in order to react to changes and perform corresponding actions, e.g. updating the stock or sending a confirmation email to the customer.

Respond to events to update other components:

Domain events allow different components of your application to react to state changes and communicate with each other without having direct dependencies. This improves the flexibility and extensibility of your application and promotes a modular architecture.

Best practices and recommendations

Naming conventions for events:

It is advisable to use consistent naming conventions for your domain events to ensure uniform and easy-to-understand naming. Use meaningful and precise names that clearly reflect the content and meaning of the event.

Documentation of events and their payloads:

To facilitate the use of domain events and encourage collaboration between developers, it is important to document events and their payloads appropriately. Describe the purpose of each event as well as the data fields it contains and their meaning.

Monitoring and logging of event-based operations:

To ensure the reliability and performance of your application, you should monitor and log event-based operations appropriately. This includes logging event publications, receptions and processing as well as monitoring system metrics and error messages.
By applying these best practices, you can improve the effectiveness and robustness of your application and ensure that domain events are used efficiently and reliably.

DomainEvents (Basic)

using System;

namespace Basics.Events
{
    public class OrderPlacedEvent : EventArgs
    {
        public long OrderId { get; set; }
        public DateTime OrderDate { get; set; }
        public string CustomerName { get; set; }

        public OrderPlacedEvent(long orderId, DateTime orderDate, string customerName)
        {
            OrderId = orderId;
            OrderDate = orderDate;
            CustomerName = customerName;
        }
    }

    public class EventBus
    {
        private static EventBus _instance;

        // Event definition
        public event EventHandler<OrderPlacedEvent> OrderPlaced;

        private EventBus() { }

        public static EventBus Instance
        {
            get
            {
                if (_instance == null)
                {
                    _instance = new EventBus();
                }
                return _instance;
            }
        }

        // Method to publish the event
        public void PublishOrderPlacedEvent(long orderId, DateTime orderDate, string customerName)
        {
            OrderPlaced?.Invoke(this, new OrderPlacedEvent(orderId, orderDate, customerName));
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
using System;
using Basics.Events;

public class Program
{
    public static void Main(string[] args)
    {
        // Subscription to the "Order placed" event
        EventBus.Instance.OrderPlaced += HandleOrderPlacedEvent;

        // Place order and trigger event
        PlaceOrder(34059020106036052, DateTime.Now, "Walter Hartwell White");
    }

    public static void PlaceOrder(long orderId, DateTime orderDate, string customerName)
    {
        // The logic for order processing would take place here

        // Triggering the "Order placed" event
        EventBus.Instance.PublishOrderPlacedEvent(orderId, orderDate, customerName);
    }

    public static void HandleOrderPlacedEvent(object sender, OrderPlacedEvent e)
    {
        Console.WriteLine($"New order received: ID={e.OrderId}, Date={e.OrderDate}, Customer={e.CustomerName}");

        // Further actions could be carried out here, e.g., send a confirmation email
    }
}
Enter fullscreen mode Exit fullscreen mode

DomainEvents with MediatR

using MediatR;
using System;

namespace With_MediatR.Events
{
    public class OrderPlacedEvent : INotification
    {
        public long OrderId { get; set; }
        public DateTime OrderDate { get; set; }
        public string CustomerName { get; set; }

        public OrderPlacedEvent(long orderId, DateTime orderDate, string customerName)
        {
            OrderId = orderId;
            OrderDate = orderDate;
            CustomerName = customerName;
        }
    }
}

Enter fullscreen mode Exit fullscreen mode
using MediatR;
using System.Threading;
using System.Threading.Tasks;
using With_MediatR.Events;

namespace With_MediatR.Handlers
{
    public class OrderPlacedEventHandler : INotificationHandler<OrderPlacedEvent>
    {
        public Task Handle(OrderPlacedEvent notification, CancellationToken cancellationToken)
        {
            // Beispielhafte Verarbeitung des Ereignisses
            Console.WriteLine($"Order placed: ID = {notification.OrderId}, Date = {notification.OrderDate}, Customer = {notification.CustomerName}");
            return Task.CompletedTask;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
using MediatR;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Reflection;
using System.Threading.Tasks;
using With_MediatR.Events;
using With_MediatR.Handlers;

public class Program
{
    public static async Task Main(string[] args)
    {
        try
        {
            // Configuration and initialisation of MediatR
            var serviceProvider = ConfigureServices();
            var mediator = serviceProvider.GetRequiredService<IMediator>();

            // Place order and trigger event
            var orderId = GenerateOrderId();
            var orderDate = DateTime.Now;
            var customerName = "Walter Hartwell White";

            await mediator.Publish(new OrderPlacedEvent(orderId, orderDate, customerName));

            Console.ReadLine();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"An error occurred: {ex.Message}");
        }
    }

    private static IServiceProvider ConfigureServices()
    {
        var services = new ServiceCollection();

        // Add MediatR
        services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()));

        // Structure of the service provider
        return services.BuildServiceProvider();
    }

    private static long GenerateOrderId()
    {
        // Dynamische Generierung einer Order-ID
        return DateTime.Now.Ticks;
    }
}
Enter fullscreen mode Exit fullscreen mode

DomaineEvents with MediatR and CQRS Pattern

using MediatR;
using Microsoft.Extensions.DependencyInjection;
using System.Reflection;
using With_MediatR_CQRS.Commands;
using With_MediatR_CQRS.EventHandlers;
using With_MediatR_CQRS.Handlers;
using With_MediatR_CQRS.Queries;
using With_MediatR_CQRS.Repositories;

public class Program
{
  public static async Task Main(string[] args)
  {
    // Configuration and initialisation of MediatR
    var serviceProvider = ConfigureServices();
    var mediator = serviceProvider.GetRequiredService<IMediator>();

    // Creation of the command to create a new order
    var createOrderCommand = new CreateOrderCommand
    {
      OrderId = 034059020106036052,
      CustomerName = "Walter Hartwell White",
      OrderDate = DateTime.Now,
      Products = new List<string> { "BlueProduct #1", "BlueProduct #2" }
    };

    // Call the handler for the command to create a new order
    await mediator.Send(createOrderCommand);// [call CreateOrderCommandHandler]

    // Creation of the query for orders from a specific customer
    var getOrdersQuery = new GetOrdersQuery
    {
      CustomerName = "Walter Hartwell White"
    };

    // Call up handler for querying orders from a specific customer
    var orders = await mediator.Send(getOrdersQuery); // [call GetOrdersQueryHandler]

    Console.WriteLine($"Number of orders for {getOrdersQuery.CustomerName}: {orders.Count}");
  }

  private static IServiceProvider ConfigureServices()
  {
    var services = new ServiceCollection();

    //Register repositories and handlers
    services.AddTransient<OrderRepository>();
    services.AddTransient<CreateOrderCommandHandler>();
    services.AddTransient<OrderCreatedEventHandler>();
    services.AddTransient<GetOrdersQueryHandler>();

    // Add MediatR
    services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()));

    return services.BuildServiceProvider();
  }
}
Enter fullscreen mode Exit fullscreen mode
using MediatR;
using With_MediatR_CQRS.Classes;

namespace With_MediatR_CQRS.Queries;

public class GetOrdersQuery : IRequest<List<Order>>
{
  public string CustomerName { get; set; }
}
Enter fullscreen mode Exit fullscreen mode
using MediatR;
using With_MediatR_CQRS.Commands;
using With_MediatR_CQRS.Events;

namespace With_MediatR_CQRS.Handlers;

public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand>
{
  private readonly IMediator _mediator;

  public CreateOrderCommandHandler(IMediator mediator)
  {
    _mediator = mediator;
  }

  public async Task Handle(CreateOrderCommand request, CancellationToken cancellationToken)
  {
    // This is where the logic for creating a new order would take place
    Console.WriteLine($"New order created at {request.OrderDate}: ID={request.OrderId}, Customer={request.CustomerName}");

    // Triggering the domain event "Order created"
    await _mediator.Publish(new OrderCreatedEvent(request.OrderId, request.CustomerName, request.OrderDate, request.Products));

  }
}
Enter fullscreen mode Exit fullscreen mode
using MediatR;

namespace With_MediatR_CQRS.Events;

public class OrderCreatedEvent : INotification
{
  public long OrderId { get; }
  public string CustomerName { get; }
  public DateTime OrderDate { get; set; }
  public List<string> Products { get; }

  public OrderCreatedEvent(long orderId, string customerName,DateTime orderDate, List<string> products)
  {
    OrderId = orderId;
    CustomerName = customerName;
    OrderDate = orderDate;
    Products = products;
  }
} 
Enter fullscreen mode Exit fullscreen mode
using MediatR;
using With_MediatR_CQRS.Classes;
using With_MediatR_CQRS.Events;
using With_MediatR_CQRS.Repositories;

namespace With_MediatR_CQRS.EventHandlers;

public class OrderCreatedEventHandler : INotificationHandler<OrderCreatedEvent>
{
    private readonly OrderRepository _orderRepository;

    public OrderCreatedEventHandler(OrderRepository orderRepository)
    {
        _orderRepository = orderRepository;
    }

    public Task Handle(OrderCreatedEvent notification, CancellationToken cancellationToken)
    {
        // For this example, we simulate the receipt of the order 15 minutes after the order was placed
        Console.WriteLine($"New order received at {DateTime.Now.AddMinutes(+15)}: ID={notification.OrderId}, Customer={notification.CustomerName}");

        foreach (var product in notification.Products)
        {
            Console.WriteLine($"Product added: {product}");
        }

        // Save the order in the repository
        _orderRepository.Add(new Order(notification.OrderId, notification.CustomerName, notification.OrderDate, notification.Products));

        return Task.CompletedTask;
    }
} 
Enter fullscreen mode Exit fullscreen mode
using MediatR;

namespace With_MediatR_CQRS.Commands;

public class CreateOrderCommand : IRequest
{
  public long OrderId { get; set; }
  public string CustomerName { get; set; }
  public DateTime OrderDate { get; set; }
  public List<string> Products { get; set; }
}
namespace With_MediatR_CQRS.Classes;

public class Order
{
  public long OrderId { get; }
  public string CustomerName { get; }
  public DateTime OrderDate { get; set; }
  public List<string> Products { get; }

  public Order(long orderId, string customerName, DateTime orderDate, List<string> products)
  {
    OrderId = orderId;
    CustomerName = customerName;
    OrderDate = orderDate;
    Products = products;
  }
} 
Enter fullscreen mode Exit fullscreen mode

In this example, MediatR is used to manage the commands and events. The CreateOrderCommandHandler is responsible for processing the command to create an order and then triggers the domain event Order created. The OrderCreatedEventHandler reacts to this event and executes corresponding actions.

This example shows how CQRS and domain events can be used together with MediatR in an application.

DomainEvents:

MediatR:

CQRS:

Top comments (0)