You may not need a query bus
Matthias Noback
“Can you make a query bus with SimpleBus?” The question has been asked many times. I’ve always said no. Basically, because I didn’t build in the option to return anything from a command handler. So a handler could never become a query handler, since a query will of course have to return something.
I’ve always thought that the demand for a query bus is just a sign of the need for symmetry. If you have command and query methods, then why not have command and query buses too? A desire for symmetry isn’t a bad thing per se. Symmetry is attractive because it feels natural, and that feeling can serve as design feedback. For instance, you can use lack of symmetry to find out what aspect of a design is still missing, or to find alternative solutions.
Nonetheless, I think that we may actually not need a query bus at all.
The return type of a query bus is “mixed”
A command or query bus interface will look something like this:
interface Bus
{
/**
* @return mixed
*/
public function handle(object $message);
}
A sample query and query handler would look like this:
final class GetExchangeRate
{
// ...
}
final class GetExchangeRateHandler
{
public function handle(GetExchangeRate $query): ExchangeRate
{
// ...
}
}
When you pass an instance of GetExchangeRate
to Bus::handle()
it will eventually call GetExchangeRateHandler::handle()
and return the value. But Bus::handle()
has an unknown return type, which we would call “mixed”. Now, you know that the return type is going to be ExchangeRate
, but a compiler wouldn’t know. Nor does your IDE.
// What type of value is `$result`?
$result = $bus->handle(new GetExchangeRate(/* ... */));
This situation reminds me of the problem of a service locator (or container, used as locator) that offers a generic method for retrieving services:
interface Container
{
public function get(string $id): object;
}
You don’t know what you’re going to get until you get it. Still, you rely on it to return the thing you were expecting to get.
Implicit dependencies
This brings me to the next objection: if you know which service is going to answer your query, and what type the answer is going to be, why would you depend on another service?
If I see a service that needs an exchange rate, I would expect this service to have a dependency called ExchangeRateRepository
, or ExchangeRateProvider
, or anything else, but not a QueryBus
, or even a Bus
. I like to see what the actual dependencies of a service are.
final class CreateInvoice
{
// What does this service need a `Bus` for?!
public function __construct(Bus $bus)
{
// ...
}
}
In fact, this argument is also valid for the command bus itself; we may not even need it, since there is one command handler for a given command. Why not call the handler directly? For the automatic database transaction wrapping the handler? I actually prefer dealing with the transaction in the repository implementation only. Automatic event dispatching? I do that manually in my application service.
Really, the main thing that I hope the command bus brought us, is a tendency to model use cases as application services, which are independent of an application’s infrastructure. And I introduced the void
return type for command handlers to prevent write model entities from ending up in the views. However, I’ve become much less dogmatic over the years: I happily return IDs of new entities from my application services these days.
No need for middleware
Actually, the idea of the command bus having middleware that could do things before or after executing the command handler, was pretty neat. Dealing with database transactions, dispatching events, logging, security checks, etc. However, middlewares also tend to hide important facts from the casual reader. One type of middleware is quite powerful nonetheless: one that serializes an incoming message and adds it to a queue for asynchronous processing. This works particularly well with commands, because they don’t return anything anyway.
I’m not sure if any of these middleware solutions will be interesting for a query bus though. Queries shouldn’t need to run within a database transaction. They won’t dispatch events, they won’t need logging, etc. In particular, they shouldn’t need to be queued. That would not make a timely answer to your query likely.
A query handler that doesn’t need middlewares, doesn’t need a bus either. The only thing the bus can still do in that case is directly forward the query to the right handler. And, as I mentioned, if there’s just one handler, and you wrote it, why not make it an explicit dependency and call it directly?
Suggested refactoring: Replace Query Bus with Service Dependency
It won’t be a surprise that my advice is to replace usages of a query bus with a real service dependency. This gives you the following benefits:
- Service dependencies will be explicit
- Return types will be specific
The refactoring in case of the GetExchangeRate
case looks as follows:
// Before:
final class GetExchangeRate
{
public function __construct(Currency $from, Currency $to, Date $date)
{
// ...
}
}
final class GetExchangeRateHandler
{
public function handle(GetExchangeRate $query): ExchangeRate
{
// ...
}
}
// After:
final class ExchangeRateProvider
{
public function getExchangeRateFor(Currency $from, Currency $to, Date $date): ExchangeRate
{
// ...
}
}
Also, every service that used to depend on the bus for answering their GetExchangeRate
query, would now depend on ExchangeRateProvider
and it should get this dependency injected as a constructor argument.
final class CreateInvoice
{
public function __construct(ExchangeRateProvider $exchangeRateProvider)
{
// ...
}
}
Optional refactoring: Introduce Parameter Object
As you may have noticed, the constructor arguments of the query object are now the method arguments of getExchangeRateFor()
. This means that we’ve applied the opposite of the Introduce Parameter Object refactoring. I find that in some cases it still pays to keep the query object. In particular if it’s something that represents a complicated query, with multiple options for filtering, searching, limiting, etc. In that case, sometimes a builder can give quite elegant results:
final class Invoices
{
public static function all(): self
{
// ...
}
public function createdAfter(Date $date): self
{
// ...
}
public function thatHaveBeenPaid(): self
{
// ...
}
}
$this->invoiceRepository->find(
Invoices::all()
->createdAfter(Date::fromString('2019-06-30'),
->thatHaveBeenPaid()
);