The most common question I get about my command bus library is: “can commands really return nothing?” The second most common question is “Why do you let commands return something, don’t you know that’s Wrong(tm)?”
It’s easy to get hung up on form, not function. That’s a shame because command buses aren’t important. In the grand scheme of things, they’re completely, utterly, totally unimportant. It’s all about where the messages are going, not how they get there.
Still, let’s take a look at some myths about command buses.
“Command Buses are a part of CQRS and they always go together.”
CQRS is about the separation of a (write) model that solves a problem and a (read) model that informs you about problems. This is a useful lens for viewing some domains.
While I wasn’t there when the term was coined (and haven’t asked), it’s pretty logical to assume the “Command” in “Command Bus” had the same etymology. It was a piece of plumbing used to address the write model, which is the important part of our app.
But that’s a one way relationship. Write model doesn’t care how you send it information. A “old school” service layer like this:
is just as CQRS-y as a command bus like this:
$this->serviceLayer->handle(new OpenAccount($email, $password));
It has the same guarantees, the same ability to encapsulate. It’s just a minor difference in implementation. And no, it doesn’t return anything.
Conversely, if we’re using either version in a non-CQRS app, it doesn’t matter if we return something or not. The non-returning is a property of CQRS, not the Command Bus.
“You can only use a Command Bus if you’re doing CQRS.”
Nowadays I would argue the term “Command” in Command Bus has less to do with the CQRS write model and more to do with the classic Gang of Four definition of “Command”, i.e. breaking down operations into discrete units and passing them around. Command Bus has found a broader audience as a modern take on implementing a service layer. And that’s okay.
The advantages for a command bus hold true whether you’re using CQRS or not:
- It’s easy to decorate the uniform interface
- It’s different enough to form a clear barrier between UI and domain model
- The command class is a nice way to capture ubiquitous language
- The command DTOs can pair well with form libraries and serializers.
“I should be using CQRS and Command Buses because that’s good practice”
No, they’re useful practices for some types of problems. CQRS is rarely a primary goal anyways. Most of the folks I see using it are actually oriented at doing Event Sourcing, which is another orthogonal concept altogether.
“I shouldn’t be returning anything because CQRS is CQS which says don’t return anything”
CQS (like CQRS) can be a useful lens for some problems but not all situations or practices. It is a good guideline for warning you against side-effect prone code. That said, CQS is aimed at method level whereas CQRS is targeting higher-level architecture. I’d argue they’re spiritually similar but CQRS isn’t direct a superset of CQS.
“We don’t return things because commands are asynchronous”
At a technical level, this seems legit. We could be sending the command into a background queue so we don’t know when it’s actually going to be executed. Therefore, we can’t count on a return value for any command.
In earlier times, this was touted as a feature. Commands are so easy to serialize, why wouldn’t we put them in queues and message buses like we do for events? At least, this is how I first understood it.
As time passed though, there’s been a general realization this doesn’t hold up. If I can’t execute the bulk of a user’s request directly because of some resource constraint, then I should still record and acknowledge they started it.
If you’re doing Event Sourcing, using a Saga or Process Manager is a very natural fit here; the initial command doesn’t do anything beyond raising an event that a user requested we do something but at least we have a firm record of it. Then using the same systems we use to distribute events to projectors, we can trigger a background worker when resources are available. If there are any problems, the Saga/Process Manager can retry using the recorded event.
In other words: let events be asynchronous, make commands be synchronous.
“We don’t return things because it’s our write model.”
Yes, now we’re getting somewhere.
The CQRS aversion to return values has nothing to do with return values specifically. It’s about making certain the write model isn’t reporting about the current state of the system, that’s the read model’s job. That’s the main supposition of CQRS, after all, that treating problem solving and reporting separately lead to less friction and more flexible models.
Whether the write model gives back state by return value, reference, pointer or passing indirectly via another object doesn’t matter. The goal is to keep the write model from taking on reporting duties.
The lack of return values is a consequence, not a first order rule. We’re not silencing the write model, it just has nothing to say.
“Right, we never return anything ever. EVER.”
We’re not having the write model generate and pass back the changed state of the app.
That said, there are instances the write model responds. In PHP or similar languages, the write model typically raises exceptions to signal error states. While exceptions are semantically and idiomatically different than return values, they can be used in a similar fashion. If we were working in a language without exceptions (like Golang), then it would be very reasonable to return errors from the command bus.
None of this violates CQRS principles. Even orthodox folks will concede that returning an acknowledgement or some other “small” value is reasonable (albeit in rare, always unspecified edge cases). In practice, the most common use case like this is returning a complex id from the command handler. While I (personally) don’t think that’s a huge problem, it’s pretty easy to work around this by introducing a domain service that can be used to pre-generate the id in the controller.
Finally, it’s important to remember that write models often produce state and make it available through other means. The most common reason for CQRS right now is Event Sourcing, which produces state as events. While events are recorded mainly for the purpose of future write model decisions, they are distributed through the system, so in the sense of availability, they’re as good as returned.
“Wait, so I can return events from my command handlers and still be doing CQRS?”
I don’t know. Maybe? But even if that was internally consistent, I wouldn’t recommend returning events all the way up to the controller dispatching the command.
Examining the event stream and deciding what to show the user can be a complex task. After all, it’s complex enough that we have the concept of a read model.
The last thing most apps need is more logic in controllers, so don’t burden controllers with the task of interpreting a partial event stream. There might not be enough information to make a good projection but there is enough complexity to make a bad one.
So, while we could get that data at the controller level, that doesn’t make it the best place to handle it. Nor does having the events there overcome the UX frustrations of eventually consistency. If that’s a problem, you don’t have scaling issues and you have a taste for the unorthodox, then flout eventual consistency of your read models however you like but just returning events isn’t the fix for that.
In the end, my advice is let the write model communicate to the read model horizontally and query the read model from your user interface vertically.
“Cool story bro but I’m using a Command Bus that doesn’t return anything so I know I’m doing CQRS right.”
Not necessarily. What if your read and your write model are still tightly coupled? That can happen regardless of any bus.
In the same way it’s possible to use a data mapper ORM and still produce tight coupling on a database, it’s perfectly possible to use a command bus that doesn’t return anything and not be doing CQRS.
Tooling never guarantees an outcome.
“So…should I be returning stuff or not?”
In a non-CQRS app? Do whatever the heck you want.
In a CQRS app? Probably not, depending on how you’ve wired it up. But remember, the point isn’t to cargo-cult about return values, it’s to understand the constraints and decisions that lead to that tactical choice.
Architectures have evolved certain conventions in the way they’re built, it’s the tradeoffs we’re trying to respect, not the specific form. At the same time, these conventions evolved because of the experience of previous developers and we can learn from them to avoid bumping into the same issues.
Thanks to Frank de Jonge, Jeroen vd Gulik and Shawn McCool for proofreading and feedback.