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.
If you’re frustrated working in a large spaghetti code system that’s hard to change and easy to introduce bugs, you’ve probably wanted to rewrite everything from scratch. The reality is that’s not likely going to happen. However, here’s how you can think about untangling the spaghetti code mess in smaller chunks one at a time, making it more manageable.
Check out my YouTube channel, where I post all kinds of content accompanying my posts, including this video showing everything in this post.
People often refer to a spaghetti code when It has a lot of concerns intertwined, and because of that, it’s hard to change. We’re talking about a high degree of coupling and indirection.
There are two aspects to coupling, Afferent and Efferent. When you end up with a high degree of both, you’ll end up with a system that’s hard to change. Check out my post, Write Stable Code using Coupling Metrics, for more on Afferent and Efferent coupling.
As many things in software architecture and design, you’re managing coupling. You’re fighting coupling with cohesion and trying to find a balance. Left unchecked this is often the root cause of why you end up with a ton of indirection, high coupling, and low cohesion.
So how do you manage coupling?
The first thing you need to ask yourself in a large system, is what does the system actually do?
What capabilities does your system provide? Where is the actual value in your system? Not all functionality in a large system has the same amount of value. There are different parts that serve different purposes. Some parts of your system are to manage reference data and act more in a supporting role. There are likely other parts that are at the core of your system. There may be many, and even to that degree, each might have a different degree of value. Other parts of your system have different responsibilities.
Take, for example, the domain of distribution. You’re purchasing and receiving products from suppliers, then storing those in your warehouse, to then be sold and shipped to customers. The core of your domain may be shipping, receiving, and warehouse management. Maybe sales, purchasing, and accounting are done in totally separate systems that are integrated. Supporting boundaries aren’t less important, but they aren’t in the solution space that you’re developing. You may still choose to develop them but they are there to support the core functionality you developing. Or as mentioned, maybe it’s something you integrate as it’s a product/service you can buy off the shelf.
Yes, you’re entire system is connected in various ways. But how you manage that coupling is what matters. It doesn’t need to be spaghetti code.
The first step is identifying what pieces of functionality relate to each other. Grouping functionality together that provides specific capabilities of your system is defining a logical boundary.
Carving off even a small subset of functionality by defining a local boundary, but also the data that logical boundary owns is important.
We only want one boundary to own and manage the data that belongs to it. This means we can’t have other boundaries or our existing spaghetti system to query or change any of our data.
If we allowed any piece of functionality to read or change data, we’re basically integrating at the database layer and it’s a complete free for all. Good fences make good neighbors. We want to establish boundaries so that any interactions are done though a well-defined API that is our contract.
Ultimately, you’re segregating functionality and defining an API for how each boundary interacts with each others.
You’re making cohesive boundaries and managing coupling.
You can take this a step further and remote the temporal aspect of coupling by using messaging. This allows us to loosely couple between boundaries. Check out my post Message Driven Architecture to DECOUPLE a Monolith.
It isn’t easy
You might be thinking of your own system and yelling, “ya right, this will be really difficult”.
Defining boundaries is one of the most difficult things to do, but also the most important.
So where should you start? One recommendation is to start is just defining on paper/whiteboard/etc at a high level, what those logical boundaries are. Don’t even get into the code, just at a high level, define what your system does and what those boundaries might be.
Then take a look at a very small boundary that is already a bit segregated and has the lowest amount of coupling already. This way, you can define that boundary in code and go forward with how you’re going to segregate and define your API. Doing this with a small boundary so you can work out all the kinks and issues that will come up along the way.
Once you’ve gone through the motions of carving out a small boundary, you can keep going by taking out other chunks. You’ll make a lot of discoveries along the way and possibly change your approach based on those discoveries. You may notice that what you originally thought was a logical boundary should be combined with another. Or on the flip side, that one is too large and needs to be split up.
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.