https://pixabay.com/users/steenjepsen-1490089

Untangling Legacy Code with Events

Legacy code is typically characterised by long methods that mix up different concepts and levels of abstraction. In this context, it can be hard to extract behaviour into proper modules because it feels like everything depends on everything and it would require a massive overhaul of the entire codebase. This article proposes to use events to reduce coupling between legacy code and other properly-bounded modules to either extract existing code or add new one.


Let’s say you have a typical UserManager class which features a CreateUser method. Once a user is created, we might want to send a welcome email to set the password and clear a cache somewhere. The initial code looks something like this:

public class UserManager {
    // ...

    public async Task CreateUser(string username) {
        var user = new User { Username = username };
        int userId = await _repository.Add(user);
        await ClearCacheForUser(userId);
        await SendWelcomeEmail(userId, user);
    }

    private async Task ClearCacheForUser(int userId) {
        // ...
    }

    private async Task SendWelcomeEmail(int userId, User user) {
        // ...
    }
}

The first step could be to extract ClearCacheForUser and SendWelcomeEmail to dedicated classes. However, it would not fundamentally change the issue. Indeed, the CreateUser method would still need to know to clear the cache, send the email, and all the other processes that need to happen upon user creation. Thus, the coupling is still there.

A simple solution to remove this coupling is to use events. Before you raise an eyebrow, it’s important to note that using events does not necessarily mean implementing a full-blown event-sourcing architecture with an event store and whatnot (keep in mind that event-driven and event-sourcing are two separate things). As a matter of fact, starting with a very straightforward in-process event bus can go a long way to help untangle your legacy code.

The following code is a complete implementation of an in-process event bus that you can use.

public abstract class MyEvent {}

public interface IEventBus
{
    Task Publish<T>(T @event) where T : EpEvent;

    void Subscribe<T>(IEventHandler<T> handler) where T : MyEvent;
}

public interface IEventHandler<in T> where T : MyEvent
{
    Task Handle(T @event);
}

public class InProcessEventBus : IEventBus
{
    private readonly IDictionary<Type, IList<object>> _subscriptions = new Dictionary<Type, IList<object>>();

    public async Task Publish<T>(T @event) where T : MyEvent
    {
        if (_subscriptions.TryGetValue(typeof(T), out IList<object> handlers))
        {
            foreach (IEventHandler<T> handler in handlers.OfType<IEventHandler<T>>())
            {
                    await handler.Handle(@event);
            }
        }
    }

    public void Subscribe<T>(IEventHandler<T> handler) where T : MyEvent
    {
        if (!_subscriptions.ContainsKey(typeof(T)))
        {
            _subscriptions[typeof(T)] = new List<object>();
        }

        _subscriptions[typeof(T)].Add(handler);
    }
}

Using this minimalist event bus, we can re-write our CreateUser method like this:

public class UserCreatedEvent : MyEvent {
    public int UserId { get; }
    public string Username { get; }

    public UserCreatedEvent(int userId, string username) {
        UserId = userId;
        Username = username;
    }
}

public class UserManager {
    // ...
    private readonly IEventBus _eventBus;

    public async Task CreateUser(string username) {
        var user = new User { Username = username };
        int userId = await _repository.Add(user);

        await _eventBus.Publish(new UserCreatedEvent(userId, username));
    }
}

Now, we can create handlers to process this UserCreatedEvent and do what needs to be done.

public class ClearUserCacheEventHandler : IEventHandler<UserCreatedEvent>
{
    public ClearUserCacheEventHandler(IEventBus eventBus)
    {
        eventBus.Subscribe(this);
    }

    public Task Handle(UserCreatedEvent @event)
    {
        // Clear cache
    }
}

public class SendWelcomeEmailEventHandler : IEventHandler<UserCreatedEvent>
{
    public SendWelcomeEmailEventHandler(IEventBus eventBus)
    {
        eventBus.Subscribe(this);
    }

    public Task Handle(UserCreatedEvent @event)
    {
        // Send welcome email
    }
}

This event bus will not help with scalability as the events are processed synchronously and the Publish method only returns once all the events are processed. However, it can dramatically reduce coupling within the codebase and can act as a stepping stone to make your architecture more event-driven. Moreover, if you need to go further (e.g. two services communicating through events), you already have the abstractions in place to switch the InProcessEventBus for an out-of-process implementation.

Comments