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.
Are you overwhelmed by technical debt? Taking the path of least resistance when implementing new features in a large existing codebase will ultimately turn it into a difficult-to-change turd pile. It’s a vicious circle. Making the “quick change” constantly makes it harder to make future changes. So what’s the solution? Being aware of technical debt, stop solely thinking about data, and give yourself options in your architecture.
Check out my YouTube channel, where I post all kinds of content accompanying my posts, including this video showing everything in this post.
Path of Least Resistance
One common reason for a system growing over time and becoming unmaintainable is developers choosing to take the path of least resistance when implementing a change.
This happens for various reasons, such as time constraints, unfamiliarity with the system, lack of domain knowledge, poor overall architecture & design, etc.
For example, let’s say we have a typical web application that is using some underlying web framework that invokes some code into our application logic, through to our domain, and then some interaction with a database.
When a new feature is implemented, it’s common to look at other features as templates for developing a new feature. Or, worse, it can be using an existing feature and adding the relevant code needed for the new feature throughout the stack. I say worse because this can often confuse two concepts that seem similar but are very distinct. Merging the two concepts within the same code path can add complexity.
This means we may change existing code through the entire stack, from the client, web API, application code, domain, and our database.
You may decide to piggyback off another feature because of time constraints. It’s not because the feature is difficult to implement. It’s time-consuming or will take more time than you have to implement. Or if you’re new to the codebase or it’s brittle, you might be afraid to make changes because you know it it can cause you to break other parts of the system and don’t want to cause any regressions.
The path of least resistance is making a change that you know isn’t going to break anything that isn’t overly time-consuming, but it’s not necessarily the ideal. It’s likely good for the right now but not good for the long run.
Technical debt isn’t inherently bad. For me, technical debt comes in two forms. The first is when you’re aware and choosing to take on technical debt at a very moment, knowing it adds value now but will cause issues in the future. This awareness of choosing to make this explicit decision isn’t bad.
However, when you’re unaware that you’re making these types of decisions is when you’re headed in the wrong direction.
If you’re making explicit decisions about the tradeoffs of technical debt, you’re aware of the debt being incurred. You can then explicitly choose when to pay off (refactor) that debt. For example, with a startup, you might incur debt right now so that you have a future.
On the other side, if you’re unaware that you’re incurring technical debt, then when would you realize all the debt that’s been incurred and needs to be addressed? Taking the path of least resistance, without realizing it, is one form of this happening. While it seems like it’s helping you now, it could be hindering you now and even more so in the future.
Coupling & Cohesion
Software Architecture is about making key decisions at a low cost that give you options in the future. Having a good architecture allows you to evolve your system over time. As a codebase and system grow, it should not hinder future development. I’ve talked about this more in my post What is Software Architecture?
Why is a system brittle and hard to change? Generally, it has a high degree of coupling from higher and lower levels within a system. I find this is often because of the focus on data and informational cohesion rather than functional cohesion.
For example, let’s say we are in an e-commerce and warehouse system. There is the concept of a product. When we primarily think about data first, we think of a singular product. It holds all information for everything related to an individual product. The name, price, location in the warehouse, the quantity on hand, it is available for sale, etc.
In reality, a system for e-commerce and a warehouse would be huge. A large codebase that multiple departments would use in an organization. Sales, Purchasing, Warehouse (shipping & receiving), Accounting, and more.
In other words, I’m simplifying this example only to show a few different pieces of data related to a product, but in reality, there would be a lot.
When focusing on data primarily, we lose sight of the behaviors that relate to this data. What does the QuantityOnHand have to do with the Price? What does the Location have to do with the Description?
We’ve lumped all aspects into one concept of a product. However, in a large system like this, the concept of a product would exist in many different forms depending on the behaviors provided.
Sales have the concept of the product that cares about the Selling Price and if we’re selling. It’s customer focused.
Purchasing cares about the price from the vendor or manufacturer, which is our cost. It’s vendor-centric.
The warehouse cares about the location of the product in the warehouse and the assumed quantity on hand.
Each logical boundary has a concept of a product but has different concerns in each of its own contexts.
This means instead of mixing all these different concerns up together, instead be driven by the capabilities of each boundary and then the data ownership for those capabilities.
Low functional cohesion will lead to a high degree of coupling.
Defining logical boundaries by grouping related behaviors will lead to higher cohesion, which can then lead to loose coupling.
Some of the trade-offs of taking the path of least resistance is being aware of the trade-offs you are making between coupling and cohesion. Earlier I mentioned piggybacking off an existing feature to implement a new feature. You’re coupling. Again, not a bad thing if that decision is explicit.
Over time, left unchecked, if you’re unaware of the technical debt you’re creating, you’ll end up with a large turd pile that’s brittle and hard to change.
If you are aware you can choose when to pay down debt (refactor) and keep making those decisions over time, you can manage the amount of debt incurred, never letting it get out of reach.
I often say a system is a turd pile because nothing is perfect. It’s a constant battle to pay down debt, whether you choose it explicitly or not.
Developer-level members of my YouTube channel or Patreon 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.