In the previous article from the Inversion of Control (IoC) series, we looked at dependency injection. If you haven’t read it yet, I recommend you to check what inversion of control actually is. Today, I’ll present the next level of implementation of the Inversion of Control: the Events.
To understand what Events are and how they differ from the dependency injection, let’s imagine that our system is a team. You lead the team and thus you are responsible for getting new contracts. Whenever you win a contract (that’s your Event), your team needs to carry out specific tasks as part of the work - it can be anything from creating documentation, recruiting new developers to preparing low-fi mockups. Most likely, you won’t be pointing out at each team member telling them what their responsibilities are, but you need to spell out what tasks need to be done and distribute them among your team members. The below graphic illustrates how this could work.
In an ideal world, however, you wouldn’t want to worry about the distribution of tasks at all - everyone on your team would have a great understanding of what needs to be done when a new contract comes and declare which tasks they would take care of (see the below graphic). That’s exactly what Events allow you to do in the Inversion of Control pattern. In this hypothetical world, you could concentrate solely about winning new contracts and passing on the details to your team. Even if your team was in the process of expansion, you could focus on contract issues and not let team issues affect your scope of work.
Coming back to our previous Inversion of Control article where we discussed Dependency Injection, we know we could be lest with the issue of coupling. In this article, we’d like to show you how to use Events to solve this problem.
Before we can investigate the Events in terms of software architecture, let’s see how Events can decouple your code. Assume we are creating a feature which gives our users a special discount on their next order.
Few sprints later, your team has developed a loyalty points feature, which gives 10 points for each item bought, but only if the user has bought at least 4 products in a single order.
So, even when OrderService is not responsible for adding discounts or loyalty points, it has to be aware of the IDiscountService and ILoyaltyService, being slightly coupled with them. As soon as our system grows up, our simple SubmitOrder method is getting larger, having more dependencies and more potential points of failure.
OrderService can also become a so-called publisher (the sender of messages), allowing all the receivers (subscribers) of that message to do whatever they need when they receive it. It can simply inform all interested objects that order has been submitted, rather than calling every single service individually. Similarly, subscribers do not require information about the publishers, if any even exists. All they care about is the message or the Event.
With Events, we can easily decouple our OrderService from other services, allowing new subscribers to listen to OrderSubmitted Events, without changing a single line of code in the SubmitOrder function.
As you can notice, OrderService is no longer dependent from IDiscountService or ILoyaltyService. The only dependency it has is service responsible for publishing Events. Also, keep in mind that the Event has an informative name, and it informs us that something has happened. It’s not a command and its intent is not to change the state of the system in any way, but to inform that state has been changed.
Events might be helpful when it comes to developing systems based on microservices. As we know, microservices should be independent, and ideally, should be unaware of other microservices in the system. We can extend our example with the order and discounts: imagine that orders, discounts and loyalty programs are handled by different and independent services. Without Events, you would probably use HTTP to trigger discount and loyalty program actions.
As a result, our OrderService is highly coupled with the Discount and Loyalty Program services, as it won’t behave correctly when these services crash. Naturally, if services are highly coupled, potential problems will be harder to solve. If a service is temporarily taken offline or completely removed from the architecture, you would have to do extra work to handle such cases.
Luckily, instead of directly communicating with those services, you can emit an Event with all the information required, allowing other services to subscribe to that Event. From that point, whenever you need to extend your system to a new subscriber of OrderSubmitted Event, you can do it without changing a single line of code within your OrderService, making it more independent and decoupled. You can send such Events using one of the message brokers available on the market, like RabbitMQ, Kafka, Azure Service Bus or the others.
To avoid unnecessary direct communication between microservices, it’s crucial to include all the details about the submitted order in the event. Subscribers should be able to find everything they need there.
Inversion of Control through Events leads us, obviously, to losing even more control. Events allow us to decouple from time, dependent object types and their number. Publishers do not need information about who is receiving the Event, when it will be consumed or what they want to do about it. While dependency injection helps us to change existing behavior of the system (allowing us to do something differently), Events can be helpful when we are expecting an extension of a part of our system, without having to modify the existing code.
Next, I’ll cover the third and the strongest implementation of the inversion of control, which is Aspect Oriented Programming. AOP is used to separate cross-cutting concerns. It allows you to add business support behavior (e.g. security) without cluttering the core business code.