Skip to content

Clean Up Bloated CQRS Handlers

Sponsor: Do you build complex software systems? See how NServiceBus makes it easier to design, build, and manage software systems that use message queues to achieve loose coupling. Get started for free.

Learn more about Software Architecture & Design.
Join thousands of developers getting weekly updates to increase your understanding of software architecture and design concepts.


We’ve all had bloated CQRS handlers. You open up a command, query, or event handler, and it’s a bloated mess. It’s a nightmare of code. There’s validation, authorization, state changes, side effects, logging, it’s a mess to maintain and it’s really hard to test.

YouTube

Check out my YouTube channel, where I post all kinds of content on Software Architecture & Design, including this video showing everything in this post.

The Bloated Handler

Now, mind you, this is a very simple example, but you’ll get the gist because there are a lot of concerns here. This example is dispatching a shipment, basically, a package.

Here’s what that might look like:

Mind you, in the real world, you could probably imagine this being hundreds of lines long with all kinds of validation, state transitions, and other logic, but you get the gist. There’s a lot going on here. This can often be pretty typical of most CQRS handlers that contain validation, state changes, and other concerns such as email and event publishing in this example.

Step 1: Move Logic into the Domain

I still have my shipment logic here, but instead of doing that validation to make sure the status was in a ready state and then changing the state, I moved that all into our shipment.

I created a Dispatch method where I just moved that logic into it.

Quick Note on Indirection

Now, hold up a minute here, because you might have watched some of my other blogs/videos where I harp on indirection.

I’m not suggesting you do everywhere. Do this when, you have another place that uses the exact same state transition. Put that logic in a central place so that you always know you’re in a valid state.

Don’t add indirection for no good reason.

Step 2: Creating a Pipeline (Russian Doll Pattern)

Having made that disclaimer, let’s go to step two, creating a pipeline so you can execute small, simple tasks that are part of your flow.

This is often referred to as the Russian Doll pattern.

If you’re familiar with ASP.NET Core Middleware, it’s the exact same idea, except this is scoped down to a single application request, like a specific use case.

That’s exactly what I’ve done in code, broken it apart to create a pipeline.

I’m not going to show all the trivial code for executing or defining a pipeline, you’re likely already using tooling if you’re working with commands, queries, or event handlers. The tooling you’re using might already support this, so check the documentation.

The important part is that I have a context.

This context is passed through my pipeline from one step to another.

Handling Side Effects

Now, I used to have logic for sending the email directly in here, but we can actually do that as part of the event instead.

That’s not even part of the pipeline, just completely asynchronous.

If we’re using some event-driven architecture, whether in-process or not, I can handle that event separately when the shipment is dispatched to notify the customer.

Benefits of This Approach

So now we’ve broken apart that original handler that had a lot of concerns into small steps, each calling the next.

And remember, because this is a Russian doll, when we call that last next, there’s nothing left to call, it returns. Then, the previous step resumes, which in our case saves the shipment to the database.

Now, everything has trade-offs.

One of the first benefits you’ll notice here is that it’s way easier to test because you’re testing a single step. You don’t have one big handler with seven dependencies. Instead, you have a small step that might have one or two dependencies that you can fake or mock easily.

That makes testing each part a lot simpler.

Another benefit is that it’s composable.

You might have certain steps that you want in every pipeline.

You might’ve noticed in the example that maybe you’d want to use the outbox pattern so that events aren’t published until after the database transaction commits. That’s a perfect fit here.

The Downside

Now, the downside is indirection, and that’s my biggest complaint about a lot of software.

If you look at a call stack, it can be a layered, nested mess. This pattern does add that.

But, like everything, there are trade-offs.

If you have complicated workflows and a handler with a ton of dependencies and hundreds or thousands of lines of code, there’s a benefit to breaking it apart like this.

It’s always about trade-offs.

Join CodeOpinon!
Developer-level members of my Patreon or YouTube channel get access to a private Discord server to chat with other developers about Software Architecture and Design and access to source code for any working demo application I post on my blog or YouTube. Check out my Patreon or YouTube Membership for more info.