Categories
Software Architecture Software Development

How to use Domain Events to write decoupled software

Learn how to use this powerful technique that helps to avoid coupling while also guiding towards the Single Responsibility and Open-Closed principles.

It is easy to be tempted to write one big method that executes business logic, saves to the database and sends an email, which gets messy fast. Luckily, it is also easy to decouple everything and keep it easier to maintain.


Imagine you have a feature to moderate users before they can use your site. When the user is approved, you send them an email with the good news.

The moderation feature is already implemented, and now you just need to plug in the email sending. How would you do it? Better yet, where would you put that logic?

You could find where the system saves the result of the moderation and, if approved, you add the code to send the email right there.

public class UserModerationService
{
    public class Moderate(ModerationData moderationData)
    {
        var user = userRepository.Find(moderationData.UserId);
        user.Moderate(moderationData.Approved);
        userRepository.SaveChanges();
        // Add email sending here
        if (user.Approved)
        {
            // ...
        }
    }
}
// The User class, for context.
public class User
{
    public Guid Id { get; private set; }
    public string Email { get; private set; }
    public bool Approved { get; private set; }
    public DateTime? ModeratedAt { get; private set; }
    public bool Moderated => ModerationDate.HasValue;
    public void Moderate(bool approved)
    {
        if (!Moderated)
        {
            Approved = approved;
            ModeratedAt = DateTime.UtcNow;
        }
    }
}

But then you would be breaking SOLID‘s O, which states that a software should be open to extension but closed to modification. You would be messing with tested and working code, and we all know how dangerous that can be.

You would also be breaking letter S, which is referring to the Single Responsibility principle. Besides saving the result of the moderation, you would also be sending an email.

It may not seem a big deal, but it’s this kind of thing the piles up, creates coupling and makes the software harder to maintain.

In the future, you may have more actions related to user moderation, and if you add it all inside a single method, or even a single class – it doesn’t matter, things get messy over time.

Domain Events

Domain Events is a technique which is very appropriate situations like these.

You register things as they happen in your software using the concept of events, and then plug-in logic that cause side-effects when these events are detected.

This would be the same as handling a button-click in a web page to make it open a popup.

The event is the fact, and you can have any number of actions related to that fact. Each one modelled in its own class, in its own isolated implementation.

The first step is to setup a base class to represent an event:

public abstract class DomainEvent
{
}

You may add properties to it, like the date on which the event took place, if that’s relevant for you.

Now, you create a class to represent each specific event of interest in your domain. In our case, we want to register one for when the user has their registration moderated.

public class UserModerated : DomainEvent
{
    public User User { get; }
    public UserModerated(User user)
    {
        User = user;
    }
}

An event is something that happened in the past, and as such it is important to name your event class also in the past.

Now we need a way to associate events to domain classes, to entities. For that we are going to use a base class:

public class Entity
{
    private List<DomainEvent> domainEvents = new List<DomainEvent>();
    public IReadOnlyCollection DomainEvents => domainEvents.AsReadOnly();
    protected void AddDomainEvent(DomainEvent domainEvent) => domainEvents.Add(domainEvent);
    public void ClearDomainEvents() => domainEvents.Clear();
}

And now we have the User class adding the event to itself.

public class User : Entity
{
    public Guid Id { get; private set; }
    public string Email { get; private set; }
    public bool Approved { get; private set; }
    public DateTime? ModeratedAt { get; private set; }
    public bool Moderated => ModerationDate.HasValue;
    public void Moderate(bool approved)
    {
        if (!Moderated)
        {
            Approved = approved;
            ModeratedAt = DateTime.UtcNow;
            AddDomainEvent(new UserModerated(this));
        }
    }
}

Done. We already have the structure in place to create classes that represent events, and a way to register these events for later processing.

Here are some events that you could have: OrderRegistered, ProductBackOrdered, PaymentCompleted… anything that represents a relevant fact in your domain, and that you want to plug-in additional behavior to.

Handling Domain Events

To handle the Domain Event, we’ll create another base structure:

public interface IDomainEventHandler<TDomainEvent> where TDomainEvent : DomainEvent
{
    public void Handle(TDomainEvent domainEvent);
}

This way, every side-effect will be represented by a specialized class. This helps testing and isolating the desired behaviors.

By implementing an interface, it is also easier to create a generic infrastructure to detect all existing handlers and execute them automatically when the events happen.

This is the class that will send an email when the user is approved:

public class SendEmailWhenUserIsApprovedInModeration : IDomainEventHandler<UserModerated>
{
    private readonly IEmailSender emailSender;
    public SendEmailWhenUserIsApprovedInModeration(IEmailSender emailSender)
    {
        this.emailSender = emailSender;
    }
    public void Handle(UserModerated domainEvent)
    {
        if (!domainEvent.User.Approved)
            return;
        var message = BuildMessage(domainEvent);
        emailSender.Send(domainEvent.User.Email, message);
    }
    private void BuildMessage(UserModerated domainEvent)
    {
        // Omitted for brevity.
    }
}

Note that the handler is named as a verb. This could easily be the name of a Use Case, which is very nice. It represents an action that will take place after the fact.

Glueing everything together

We’re missing the engine that activates the handlers when the events happen.

But a question remains: when should the handlers be executed? Right before the entities are persisted? Right after?

The question is relevant because there’s still room for errors. Imagine there’s been a problem persisting the approved user to the database. What if the email is sent before the call to SaveChanges? The user could get the email, try to log in and be blocked with an error message saying that they haven’t been approved yet.

At the same time, you may have some reason for needing the side-effects to take place before anything is persisted.

As with everything in computing, it depends. But in my experience, dispatching the events right after persisting the data has proved a nice strategy until now.

We’ll have a separate class just for dispatching the events, connecting them to their handlers.

public class DomainEventDispatcher
{
    private readonly IServiceProvider services;
    public DomainEventDispatcher(IServiceProvider services)
    {
        this.services = services;
    }
    public void Dispatch(DomainEvent domainEvent)
    {
        var wrappedHandlers = GetWrappedHandlers(domainEvent);
        foreach (DomainEventHandler handler in wrappedHandlers)
            handler.Handle(domainEvent);
    }
    public IEnumerable<DomainEventHandler> GetWrappedHandlers(DomainEvent domainEvent)
    {
        // This is where we get a list of all handlers for a specific type of domain event.
        // The handlers must have been registered to the IoC container during application startup.
        // Here we're using Aspnet Core's IServiceProvider, but of course you can use your own container.
        var handlerType = typeof(IHandle<>).MakeGenericType(domainEvent.GetType());
        var wrapperType = typeof(DomainEventHandler<>).MakeGenericType(domainEvent.GetType());
        var handlers = (IEnumerable)services.GetServices(typeof(IEnumerable<>).MakeGenericType(handlerType));
        var wrappedHandlers = handlers.Cast<object>()
            .Select(handler => (DomainEventHandler)Activator.CreateInstance(wrapperType, handler));
        return wrappedHandlers;
    }
    public abstract class DomainEventHandler
    {
        public abstract Task Handle(DomainEvent domainEvent);
    }
    public class DomainEventHandler<T> : DomainEventHandler where T : DomainEvent
    {
        private readonly IHandle<T> handler;
        public DomainEventHandler(IHandle<T> handler) => this.handler = handler;
        public override Task Handle(DomainEvent domainEvent) => handler.Handle((T)domainEvent);
    }
}

The DomainEventDispatcher is activated by the class that persists data to the database.

If you’re using Entity Framework, you just need to overwrite SaveChanges, like this:

public class AppDbContext : DbContext
{
    private readonly IDomainEventDispatcher dispatcher;
    public AppDbContext(DbContextOptions<AppDbContext> options, IDomainEventDispatcher dispatcher) : base(options)
    {
        this.dispatcher = dispatcher;
    }
    public override int SaveChanges()
    {
        var result = base.SaveChanges();
        var entitiesWithEvents = ChangeTracker.Entries<Entity>()
            .Select(e => e.Entity)
            .Where(e => e.DomainEvents.Any())
            .ToArray();
        foreach (var entity in entitiesWithEvents)
        {
            var events = entity.DomainEvents.ToArray();
            entity.ClearDomainEvents();
            foreach (var domainEvent in events)
                dispatcher.Dispatch(domainEvent);
        }
        return result;
    }
}

Further reading

Domain Events is heavily used in Domain-Driven Design, and often appears in architectures like Clean Architecture, Hexagonal Architecture and Onion Architecture (which are all very similar in a number of ways).

This is somewhat an implementation of the Observer Design-Pattern.

Steve Ardalis, Microsoft MVP, has put together a stater project for anyone wanting to get up and running with the Clean Architecture. You can find it at GitHub, and it also uses Domain Events just like we just saw.

Conclusion

You may need some time getting used to Domain Events, but it’s a powerful technique that greatly helps avoiding coupling while also guiding towards the Single Responsibility and Open-Closed principles.

By Phillippe Santana

Passionate about writting code that people can understand, I'm a software developer, a project manager, an entrepreneur, and people/culture enthusiast. Find me on [Linkedin](https://www.linkedin.com/in/phillippesantana/) and on [Medium](https://medium.com/@phillippesantana).