skip to content
busstop.dev

When all you have is a handler...

/ 7 min read

Everything needs to be a message!

I have worked on a software stack that has heavy use of async messaging (using NServiceBus). If you asked the devs, they would probably describe it as a heavily distributed, event driven system, and I agree with that.. but not in a positive way. I personally think that the usage of NSB/messaging is very over done. I personally think that the second most impactful decision mistake was the failure to establish a standard for this.1

I understand the appeal. These frameworks are genuinely powerful, and when you first experience the simplicity and elegance of decoupled message handlers, it is tempting to reach for them everywhere. But I think async messaging is a specific tool for a specific class of problems.

The key feature of async messaging

I think if you strip out everything, at its core, async messaging is characterized by having a broker. This has the following positive effects:

  • temporal decoupling - sender and receiver don’t have to be online at the same time to communicate
  • location transparency - senders don’t need to know where their receivers are or how many there are; they address a queue or topic rather than a specific endpoint.

The key tradeoff for the temporal decoupling specifically is that all operations are fire and forget, it is not possible to get feedback in a synchronous way. There are ways to design around this of course, but that is not the point.

Where it genuinely shines

I think that the clearest and most defensible use case is event publishing — broadcasting that something happened without the publisher needing to know or care who is listening. This works best across domain boundaries, where one domain is making a statement of fact (“an SMS was sent”) and other domains can react to it independently. The publisher has no dependency on the subscribers. This leverages both of the positive effects that I described earlier.

One place where it fits okay, is fire-and-forget work: something where the sender genuinely does not need to know what happened. In practice, I think this is rarer than people assume. Most things that look like fire-and-forget actually do need some form of feedback — even if it is just “did this fail?”. But the cases where it is truly warranted exist, and async messaging works because the negative tradeoff is a nonfactor.

Where it creates problems

Anything that requires a response

This one should be obvious, but I have seen examples where people try desparately to make everything into a handler, and they use complex mechanisms to get back a response. Async messaging is in direct opposition to any use case that requires immediate feedback. UI interactions, external API calls that block on a result, operations where the user is waiting — none of these belong in an async pipeline. Pushing them there requires you to bolt on a response mechanism anyway, which gives you the worst of both worlds: the complexity of messaging with the latency of waiting.

Internal domain coordination

This one is most likely controversial, but I have come to see it as almost always a sign of bad design. The argument goes: we have multiple things that need to happen, and messaging lets us coordinate them loosely and retry each one independently if something fails.

My experience is that when you actually work through this, the need for that kind of coordination usually points to something that could have been structured more simply. If multiple things need to happen, just do them. If the work is long-running or requires steps that you want to retry individually, messaging is still probably not the right tool — it is just the most available one.

Sagas

I am specifically talking about NSB sagas (which IMO are not real sagas). I think it looks good in theory, but in practice is easy to misuse because of how it is used. Sagas (from what I discovered from reading about the topic) is about maintaining data consistency across domains without resorting to distributed transactions/multi phase commits. I think an NSB saga would be good, if that’s what you use it for. But in practice, I don’t see them used for this… instead, I see it used like a job scheduler because the API it presents lean in that direction, without actually giving you all the tools you need. I think that last problem (lacking appropriate tools) is very underrated, and is a problem that might not reveal itself for a long time. This is also one of the cases of internal domain coordination (see above) and in my opinion, is almost always a code smell.

Scheduling

Very similar vein to my issue with sagas, using a message broker to schedule future work is particularly painful to work with. The typical approach — publishing a delayed message — produces something that is essentially invisible. You cannot easily query what is scheduled, modify it, cancel it, or get any kind of overview of what the system intends to do and when. Most frameworks treat scheduled messages as second-class citizens. If scheduling is a real requirement, it deserves a real tool.

Fire and forget as a default

There is a common pattern I want to call out specifically, because it sounds principled but tends to go wrong. When subscribing to an event from another domain, rather than putting the business logic directly in the event handler, a common pattern is to create a command and send it to themselves. The reasoning is that the command handler can then be reused by other callers.

On the surface this seems reasonable, but in practice it does not hold up. The core assumption is that the command is fire-and-forget — but most things you can do in your system are not. Most operations need some form of feedback. Most of the things that genuinely do not need feedback are exactly the effects that come from reacting to events from other domains, and those live naturally in event handlers.

My preference is the opposite default: expose an API that is synchronous request/response by default. Event subscriptions can call into that API exactly like any other caller. There is no downside — you get reuse without having to design everything around fire-and-forget semantics that most of your callers do not actually want.

Better tools for the job

Synchronous request/response

Good old HTTP or gRPC covers the majority of use cases that get incorrectly routed through messaging. Most user-driven and externally-triggered operations need a response. The caller needs to know what happened. Synchronous request/response is the easy stupid answer for that. I think the problem why people tend to avoid this is because of over compensation since http has had the same issue before.. where HTTP was used for EVERYTHING and a single request would actually cascade into a bazillion ones causing hard to debug timeout issues.

Like the other tools here, you can definitely abuse this so it is very important to be mindful when crossing boundaries. The dependency direction is especially important, but yeah, considering http instead of always defaulting to messaging can only do you good.

A job scheduler

This is an honorable mention because, not having a good job scheduler is one of the leading causes of tool abuse (citation needed lol). Jokes aside, I think its true. Its easy to reach out for that sweet saga or schedule a message, if you have no easy way to do it otherwise.

If what you actually need is to run work in the background, sequence multiple steps, prioritize work, schedule things in the future, handle recurring schedules, track the progress of a running job, or retry individual steps in a sequence — that is a job scheduler. There are tools built specifically for this. They give you visibility into what is queued, what is running, what has failed, and what is coming up. They let you inspect and modify the schedule at runtime. I think it is important to consider this too, when you are thinking about requirements like this.


The thing that frustrates me about overusing messaging is not the tool itself — it is genuinely useful when applied to the right problems. What frustrates me is that the operational cost is hidden. A system wired together entirely through message handlers looks fine on a diagram, but becomes increasingly hard to trace, debug, and reason about over time. All I’m saying is that there are other tools…

Footnotes

  1. The first, and arguably most impactful, was blurry — and often non-existent — domain boundaries.