Software development can get complex. The requirements can be more or less specific, and they tend to change as the project advances. Depending on the application itself, some changes are related to domain logic, and others are domain logic side-effects.
While we are mainly focused on the domain (we spend extra time making the domain logic flexible and easy to change), side effects can get way less attention, as they are often added long after the domain logic is completed and delivered. After a few months of orphaned module, the product owner walks in and says:
“Let’s just send an email when we update this field, to let our coworkers know we are up to no good… Aaaand we need it by the end of the day.”
For projects that don’t have a test suite in place, any change—no matter how trivial—will make us feel hesitant about making it, especially if we aren’t intimate with the code base.
Best possible scenario is not having to change a single line of code and instead just adding new functionality. If we create a new class and just place the right method call in the right place, we have changed the existing code in two places: method call and class injection.
If we decorate or proxy the class, we have to change the factory or dependency injection container/configuration. If we visit a class, we still need to instruct the visit and change some code.
While not having to change the code to add new functionality sounds pretty unrealistic, it is very easily achievable. We just make the domain logic tell the rest of the system a relevant event occurred. Make all domain services depend on the message system, and domain message handlers handle specific domain messages. In statically typed languages, we can even enforce the single responsibility principle on domain message handlers to handle one type of domain message.
To help you visualize what is going on, here is a simple diagram:
The obvious benefit is that the business logic/domain logic is absolutely oblivious to other things that it causes. But, trust me, there are more important benefits that have far surpassed my expectations.
The first and most important one was that new team members felt confident enough to add a feature to a decent size legacy code base. All of them did it quickly and without breaking anything. The fact that they could handle the task in no time, in a legacy code base, boosted their morale. Yes, you read that right.
Another benefit is decoupling domain logic from side-effects, so domain logica and business rules always stay clean. This becomes important as the project matures. Business logic classes have little dependencies. They no longer depend on domain irrelevant things. They aren’t really interested in the if, why, and how something else should happen.
Side effects to domain logic are now separated. And each does one and one thing alone. In a legacy project, separating side-effect from domain logic is part of refactoring. It’s baby steps, but the code looks cleaner, and the team feels confident they can contain the beast.
The message system was especially useful for handling redundant data in MongoDB. Again, domain logic was oblivious to redundant data. It should not care about it. We can very easily achieve immediate, eventual or no data consistency at all. All of those are the responsibility of the domain message listeners and, because they handle one message for one purpose, we can change the behavior in one place (by providing a new listener) without affecting the rest of the system.
On domain object creation, you might want to create extra objects, such as some defaults user can change later. For example, on account creation, create his company with the account name, email, etc. When deleting a domain object, you can store the deleted object in some archive, provide undo operation or delete redundant data, while keeping the domain logic clean.
If you care about business intelligence, you might not want ever to delete or update data, as you are losing business intelligence, so you want to store it in a separate database and keep the main database always clean and up to date. Domain messages make doing that simple.
Side effects don’t even have to reside in our application. Maybe we need to inform a third party about account changes, such as billing address, emails, etc. In grassroot development, we don’t need to worry should we create a separate microservice for this side-effect or not. We can create it in the monolithic application and move it when our app grows as the only caller to that service will be a message listener. In other words, it’s not coupled with domain logic.
The real issue is what domain message to broadcast and when. In case of doubt, calculate the cyclomatic complexity, and you got the domain message count for a certain procedure. Collections can be broadcasted altogether. Some sensitive information—such as passwords, credit card numbers, and PINs—should never leave the domain service as a domain message. It can pose you more trouble than gain, so be extra careful. Usually, you want to know when something has been created, updated or deleted.
Be aware that broadcasting directly from the domain service will violate the single-responsibility principle. Decorate each domain service with a message broadcaster to keep it S.O.L.I.D.
There are numerous scenarios where domain messages can be found useful, but in the end, no method will ever fit all needs. We need to be aware of the choices we have so that we can choose the one most suited for the problem at hand.