I've read an interesting blog post today about custom exceptions (here - unfortunately it's in Polish), where the author advocates for using "business exceptions".
The example comes from a dialling application, so there are the following exceptions:
- CallAlreadyInProgressException,
- IncorrectNumberException,
- NumberAlreadyDialedException
When I first looked at the code, I nodded in agreement. Given such domain, those exceptions make total sense to me. I'd implement it the same way.
When I linked to this post in our team chat, Mirek (who has more knowledge about DDD and CQRS than I do) said that it'd be better to implement with domain events. This surprised me a bit, as when the topic of custom exceptions is brought up, domain events are rarely shown as an alternative.
What's the difference between custom exceptions and domain events?
Look at this code:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# CUSTOM EXCEPTIONS | |
class CallAlreadyInProgress < StandardError | |
end | |
class Dialer | |
def call(number) | |
raise CallAlreadyInProgress.new if call_already_in_progress? | |
end | |
end | |
dialer = Dialer.new | |
begin | |
dialer.call(number) | |
rescue CallAlreadyInProgress | |
ui_notifier.send_message("Call already in progress") | |
end | |
# with domain events | |
class CallAlreadyInProgress < Event | |
end | |
class ConnectionEstablished < Event | |
end | |
class Dialer | |
def call(number) | |
if call_already_in_progress? | |
CallAlreadyInProgress.new.publish() | |
return | |
end | |
ConnectionEstablished.new.publish() | |
end | |
end | |
# those two can be in different places of our project | |
event_bus.register(CallAlreadyInProgress, ui_notifier.send_message("Call already in progress")) | |
dialer.call(number) | |
In the code, it means that when you call dialler.call(number), you're not directly interested in the result, even to the point, that the method can fail (raise an exception). You made a command and that's it.
Now, obviously, you do need to know about the failure. That's why you publish an event, using whatever mechanism under the hood. It can be a simple singleton EventBus class, which just keeps a map of objects interested in certain events and notifies them if any such event happens. In our case, we could have a UINotifier class listening to the CallAlreadyInProgress event and sending a special UI message to the user of the system (the technical details are not important here - it can be via polling or Web Sockets).
There is another difference - with events we need to "publish" the fact that all went fine. What was implicit with exceptions (no exception is raised - success), here needs to be explicit. We publish the "ConnectionEstablished" event.
This creates a nice simplification of the whole code around it. It may make it a bit more indirect, but it's actually very simple. All of the pieces of code involved do just one thing.