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.
We’ve all written long procedural code with many logic branches that must handle failures because it tries to execute a long business process with many actions. Better alternatives exist than writing a rat’s nest of complex procedural code.
YouTube
Check out my YouTube channel, where I post all kinds of content accompanying my posts, including this video showing everything in this post.
Procedural
We’ve all probably written or looked at code similar to the following. It does a few things: first, it adds and saves an order to the database; then, it tries to process the credit card payment; and lastly, it sends a confirmation email.
You’ll notice a couple of things. If the credit card request times out and throws an exception, we then mark the order as Cancelled, save it, and exit the method. We also have to wrap the sending of the email confirmation in a try/catch because we don’t want this entire thing to blow up. After all, the order was saved successfully, and we’ve charged the credit card.
You can argue that it doesn’t follow single responsibility, separation of concerns, etc. That’s all true, but even if it were, that wouldn’t solve the issue. The root problem is that we don’t have distributed transactions. You have multiple operations that are a part of a workflow against distributed components.
This is a fairly simple example, but you can probably relate to and think of your context, where you have many different independent operations that are part of a larger workflow. And it’s usually a heck of a lot more complex because there are many different branches that the code executes depending on what happens.
In our happy path, everything works great. But those try/catch blocks (used as control flow) or if they were conditional statements on return values indicate workflow. Specifically, we want each operation to execute independently from each other.
Each step of the process, working independently, can then dictate the next operation that happens. In the example of the payment processing failing, we then have the logic to cancel the order.
But we should also be sending out an email notifying the customer their order was cancelled. Where does that code live in my above example? Do we need to wrap it as well in a try/catch incase sending the email fails? This is a nightmare if that’s the case.
This is how workflows and business processes that are long procedures end up as a rat nest of complexity that is hard to follow, manage, and change.
As mentioned, you can separate all this into different classes/functions/methods to keep the code police from yelling at you, but that doesn’t solve the underlying issue because of the distributed nature of the workflow.
Sure, you want separation of concerns. But in order to maintain any consistency in a workflow that can have many different code branches, you need everything to execute in isolation.
Workflow
So what’s the solution? Workflows, more specifically, tooling that supports developing asynchronous workflows. If you’re in the .NET space, there are a bunch of great tools, such as NServiceBus, that support workflows. If you’re not using .NET, there are other tools that have SDKs for various platforms, such as Temporal.
Ultimately, they use queues and messaging and various messaging patterns to add resilience and fault tolerance around the individual tasks of your workflows.
Here’s an example of an NServiceBus Saga. It maintains the state of our order. This sample makes a couple of interesting points. First, the saga gets started when a PlaceOrder message is queued. The very first thing it does when the PlaceOrder message is handled is set up a timeout in 5 seconds so we can cancel the order. From there, it sends a ProcessPayment message. The reason for the timeout is that if, for whatever reason, we haven’t processed the payment in that time (remember this a demo/sample), we can cancel the order. When the ProcessPayment is handled, it then sends a EmailConfirmation message. Finally, when that is done the Saga is finished by calling MarkAsComplete().
This is important because each Handle (or Timeout) method is called asynchronously. None of this is procedural, and there is no temporal coupling (besides the timeout). Also, because of the defaults defined by NServiceBus, one of the messages could be processed and fail, and it would automatically be retried. You’re not writing all this logic for all these various conditions as we were with the procedural code.
This is using queues behind the scenes which provide various ways to handle failures (retries, backoff, deadletter, etc). Sure you could implement this yourself or use a library like Polly to define policies for retries, but generally this is built directly into a message library. You don’t have to write this code or plumbing.
Check out my post 5 Tips for Building Resilient Architecture that talks more about these concepts.
Here’s another of the same concepts but using Temporal. It uses the concept of workflows and activities.
Procedural Code Nightmare
If you’re in a codebase with a lot of procedural code that is really a series of operations that are part of a long-running workflow, consider a messaging library or workflow library to help with the heavy lifting of adding resilience.
There are great tools out there built for this purpose. If you ignore them, you’ll likely end up trying to write your own half-baked solution to add resilience or create more of a rat-nest mess of conditional code to handle failures in a workflow.
Business processes and workflows are everywhere, and they’re generally asynchronous. Embrace the asynchrony.
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.