Software Architectures examples: Event-Driven
We can discuss a lot about what is or what is not Event Driven Architecture. But in this post, we will see how events can improve the design in our solutions.
When we talk about events, we can refer to many different things, depending on the context. In this case, we will talk about how modeling and publishing domain events in our applications can give us a lot of advantages.
We can define our domain events as state changes in the domain state, this means “things that happen” in our domain. For example: PlayListHasBeenCreated or TrackHasBeenAddedToPlayList.
From an architecture abstraction level, if a system produces an event, maybe other parts of the system or even other systems can easily obtain that event information and perform other tasks based on that information.
Benefits
- We can trust in events. As you can see, they are verbs in past tense. This is because, event generations are produced after state changes, and this means, after all validations and conditions to perform a state change. So we can trust in event data.
- We get low coupling between layers and systems because now, the coupling between systems or use cases are only an event.
- We improve the Open/Closed Principle (OCP), because if we want to add functionality based on a domain event, we can easily react to that event subscription and perform another task.
- We improve the Single Responsibility Principle (SRP) in our use cases. Because now, we do not need to think in our use cases as big transactional tasks. Now we can chain tasks using events. This means that now we can have more pure use cases execution, delegating the side effects like sending an email or notifications to other parts of the system. This results in more fault tolerance and scalability.
- Encourages application and processes to be asynchronous so, in the end, the solution will be more versatile.
- Better monitoring what it is going on with the application domain.
Drawnbacks
Modeling and adding code to publish these events will add more cost to write the application code. And also much more complexity. The domain must embrace asynchrony, and this is not always be possible.
Example
Following this aspnetcore example, consider the next code distribution:
This music application example is built on top of the previous CQRS example. It’s recommended to understand previous examples first. In this case, we had included some projects like MyMusic.Domain.Events and MyMusic.Application.EventHandlers:
If we look at the project dependency tree, we can notice that the read part has not been affected by the decision to add events. In this case, to simplify the examples we decided to keep folder distribution related to previous examples.
Events
Class: MyMusic.Domain.Events.PlayListHasBeenCreated
public class PlayListHasBeenCreated : Event {
public string PlayListId { get; }
public string PlayListName { get; }
public PlayListHasBeenCreated(string playListId, string playListName) {
PlayListId = playListId;
PlayListName = playListName;
}
}
As you can see, in this case, events contain just the data related to the domain state change.
Events subscription
Class: MyMusic.Configuration
private static void RegisterPlayListEventConsumersInTo(EventPublisherPort eventPublisher, PlayListEventConsumer playListEventConsumer) {
eventPublisher.Register<PlayListHasBeenCreated>(playListEventConsumer.Consume);
eventPublisher.Register<PlayListHasBeenRenamed>(playListEventConsumer.Consume);
eventPublisher.Register<PlayListImageUrlHasChanged>(playListEventConsumer.Consume);
eventPublisher.Register<PlayListHasBeenArchived>(playListEventConsumer.Consume);
}
As you can see, for this example we had decided to create an in-memory event publisher, found in EventPublisherInMemoryAdapter. In this part, we manually register the events we are going to deal with in this application, and who is it going to consume them.
Events creation
Class: MyMusic.Domain.PlayList
public static PlayList Create(string id, string name) {
var playList = new PlayList(id, name, PlayListStatus.Active, new List<Track>(), null);
playList.Create();
return playList;
}
private void Create() {
events.Add(new PlayListHasBeenCreated(Id, Name));
}
It’s important to understand that events are part of the domain too, so only the domain should be able to generate them. Also domain owns domain rules, and events can only be generated if domain rules are met.
Events publishing
Class: MyMusic.Application.Services.CreatePlayListService
public class CreatePlayListService {
private readonly UniqueIdentifiersPort uniqueIdentifiers;
private readonly PlayListPersistencePort playListPersistence;
private readonly EventPublisherPort eventPublisher;
public CreatePlayListService(UniqueIdentifiersPort uniqueIdentifiers, PlayListPersistencePort playListPersistence, EventPublisherPort eventPublisher) {
this.uniqueIdentifiers = uniqueIdentifiers;
this.playListPersistence = playListPersistence;
this.eventPublisher = eventPublisher;
}
public Either<DomainError, ServiceResponse> Execute(string playListName) {
var newPlayListId = uniqueIdentifiers.GetNewUniqueIdentifier();
var playList = PlayList.Create(newPlayListId, playListName);
playListPersistence.Persist(playList);
eventPublisher.Publish(playList.Events());
return ServiceResponse.Success;
}
}
As you can see, we are using EventPublisherPort for publishing the domain events generated by playList.
Events consumption
Class: MyMusic.EventConsumers.PlayListEventConsumer
public class PlayListEventConsumer {
private readonly PlayListEventHandlerCreator playListEventHandlerCreator;
public PlayListEventConsumer(PlayListEventHandlerCreator playListEventHandlerCreator) {
this.playListEventHandlerCreator = playListEventHandlerCreator;
}
public void Consume(PlayListHasBeenCreated @event) {
var playListHasBeenCreatedEventHandler = playListEventHandlerCreator.PlayListHasBeenCreated();
playListHasBeenCreatedEventHandler.Handle(@event);
}
}
I decided to create this consumer abstraction on top of handlers to deal with injections and to generate a flow as similar as possible to the traditional controller-services approach. In most cases, I prefer to deal with deserialization and handlers construction by separating them in the delivery layer, but this part depends a lot on what tools the team has decided to use.
As we registered earlier, now when an event is triggered. A related consumer is executed, calling an event handler. In this example, we had decided to treat events handlers as use cases, at the same abstraction level as services.
Events handling
Class: MyMusic.Application.EventHandlers.PlayListHasBeenCreatedEventHandler
public class PlayListHasBeenCreatedEventHandler {
private readonly PlayListNotifierPort playListNotifier;
public PlayListHasBeenCreatedEventHandler(PlayListNotifierPort playListNotifier) {
this.playListNotifier = playListNotifier;
}
public void Handle(PlayListHasBeenCreated @event) {
playListNotifier.NotifyPlayListHasBeenCreated(@event.PlayListId, @event.PlayListName);
}
}
This means, that handlers, will be provided with ports, and potentially can deal with domain or just orchestrate infrastructure. In this case, just sends a notification.
Considerations
I see a lot of benefits in investing in events in many applications. But as always we need to use it only when it’s really needed.
On the other hand, if a team decides to start an event-driven approach, I think it’s highly recommendable to treat event publishing as a habit. Making it part of the definition of done, even if there are any subscribers to an event yet, it will be more flexible and generates a huge benefit in the future.