Change Coupling:
Why One-Line Fixes Touch Ten Files
Change Coupling is a sense-making tool for decision makers, not a quality metric to optimize. Perfect coupling alignment isn't the goal - understanding your system's real structure is. Every architectural decision involves trade-offs that must be considered in context.
The Scattered Change Problem
The Pain Points
We've all been there. You need to add one small feature but end up touching 12 files across 6 directories. What should be a simple bug fix cascades into changes you never anticipated, or worse learnt to expect and loathe. Change Coupling becomes obvious when you notice that half of your code reviews involve files that ought to be completely unrelated to the intended change.
Poor Change Coupling slows us down when reading and writing code, but worse the collective cognitive load ends up overwhelming the people trying to make the changes.
Ignoring Change Coupling leads to unpleasant surprises. We would expect that if two bits of code don't call or reference each other, they ought to be unrelated. Yet Change Coupling shows us that often things which seem unrelated have hidden needs to change anyway to make the system work. This happens for both technical and business-logic reasons. Changing X means we change Y, regardless of what the code directly implies. Static code analysis can't directly detect these dependencies, but we can develop a sense for it by working in a code base over an extended period of time.
If you want to improve the rate of change in your project, it is critical that you understand Change Coupling of your own software.
What kind of Coupling?
The code we write is merely the surface layer of what we need to think about when writing code.
Many Types of Coupling Exist
- Physical Coupling: How close code is in your file structure
- Logical Coupling: Functions that depend on each other conceptually
- Temporal Coupling: Things that must happen in sequence
- Data Coupling: Shared data structures and formats
- Control Coupling: One module controlling another's behavior
- Change Coupling: Code that frequently changes together (our focus)
Not all coupling is bad. Coupling provides necessary structure, but too much impedes change. Like a car engine, some parts must be coupled whilst others must remain independent. You wouldn't want windshield wipers coupled to the brakes.
There's no ideal coupling ratio to optimize for. Both 0% and 100% coupling are problematic. At 0% nothing works together, at 100% it is worse than a big-ball-of-mud. This analysis is a sense-making tool to help you and your team understand your system's forces and guide your architectural decisions, not chase perfect metrics or eliminate coupling entirely.
Change Coupling is interesting because it can't be detected by reading the latest
lines of code, it requires the change history. It shows the reality of how systems evolve, not how they are
wired together. This can show surprising connections between seemingly
unrelated code, or help you tidy up messy dumping grounds (often "/utils") where everything hard
to find a home for goes but it is not clear what needs to be moved out and
where.
Lastly, it is important to understand that Change Coupling relates to "code that changes together", not "code that changes vs code that does not change". When code does change, does it change together or not? This provides a very different insight than "does this code change at all?", which can indicate evolutionary stability (which is its own deep topic that would be too much to explore in this article).
Things That Change Together
Change Coupling reveals the fault lines where your system wants to split and merge. Think of it like gravity or magnetism. Code that changes together shares conceptual weight that pulls it toward the same location.
Code that never changes together
Code that never changes together despite proximity suggests artificial coupling
that may no longer serve a purpose.
The Physical vs Change Coupling Matrix
If we consider "Physical" to mean the file location of code, we can consider physical coupling of code against Change Coupling to think of various situations our code might fall into and if it is generally in a maintainable low-cognitive-load state. While this 2x2 is only meant for guiding sense-making and not an official perfect judgement, it is still extremely helpful and often shows you the problem areas to focus on, or even how to fix the problems. It can even show you what code seems to be fine regarding Change Coupling even if it seems wrong on some other measures, which could inform your designs if you are planning a conceptual rewrite of the system. The existing system might have gotten something important right.
The Change Coupling Matrix
Follow the Natural Boundaries: Where your code wants to live vs. where it actually lives
🔴 CONSIDER CONSOLIDATING
Code that changes together but lives apart. These functions are telling you they want to be neighbors.
🟢 SYMETRICALLY COUPLED
Code that changes together and lives together. This more ideal. Your physical code layout is symetric to code changes.
🟢 SYMETRICALLY DECOUPLED
Code that rarely changes together and is not stored in the same place. Clean separation that reflects actual independence.
🔴 CONSIDER SPLITTING
Code that lives together but rarely changes together. May be artificially coupled from historical decisions.
Example: Payment System
In this example a team believes they have a clean separation of concerns in their payment system. They have one module to handle actual monetary transactions, another module for calculating fees, and a third module to coordinate the flow of fees to this specific payment system.
The design intentions were that the team would only need to change one module at a time, and usually just the Fee Engine. Yet the developers sense that every Fee Engine change cascades into a change of all three modules somehow. A change analysis report confirms that they frequently change all three modules at the same time. The conceptual level is sound, but when looking at how the modules are changed coupled we now get a pretty strong indicator something is wrong before we even look at the code.
"Things that change together belong together" does not literally imply that because these three change together we should put them all in one file, rather, it is a sense-making guide for us to question what causes them to be magnetized together.
There are a multitude of reasons why the real implementation is coupled this way. Here are some examples:
- Logic for Compliance rules around transaction logging was not accounted for in the original implementation, the persistence is in the transaction processor but it needs updates to a lookup table for reason codes.
- Different fee structures in some countries require special data structures in the transaction block, it cannot be a single flat number in this financial system.
- There are merchant agreements with fee limits, so to ensure no overages happen fees are capped in the transaction processor as a safe guard.
- Database structure couples the three modules in an invisible way
- Overloading the transaction module to manage real transactions with internal "customer credit accounts".
- ...And we could keep ideating situations all day. Real systems have real problems.
Consolidating all three modules into one is tempting, but it will reduce other valuable code quality and architecture concerns. That would likely generate a monolithic mess which is about as hard to work on. Instead, we should look at how to re-structure the reasons change is introduced and extract out the common bits, or inject in the common bits. One good approach (and there are many others, but we won't discuss them all) could be to use Dependency Inversion (which is not the same thing as Dependency Injection, but may often use Dependency Injection) to isolate the business rules from the abstract machinery. Like using interchangeable drill bits instead of buying a new drill for every type and size of screw.
Alternatively, there are many times where it is absolutely the right thing to
merge two bits of code into one, or even replace part of a system. Context of
your specific coupling challenge and what you are optimizing for can lead to
many different solutions, including keeping the change-coupled-code if the
trade-offs are worth it! The more unstable your environment and business domain
clarity, the more likely it is that what's true today can quickly flip to false.
However, the key to working with change coupling is not fighting it head-on with raw effort every day, and it is also not creating a monolith. Likewise, splitting everything up into individual modules or micro-services doesn't solve it. Remember, Change Coupling is what software happens to have changed at the same time, it extends beyond the boundaries of programming languages, repositories, deployments, and interfaces. If you change two things at the same time, they are change coupled, regardless of what those two things are or where they live.
Change coupling analysis doesn't dictate solutions—it reveals the forces of attraction and repulsion acting on your system. Understanding these forces helps you make informed architectural decisions rather than fighting against the natural evolution of your codebase.
What People Get Wrong
The Silver Bullet Trap
When teams experience change coupling challenges, they often reach for:
- Event-driven architectures: "We'll decouple with events!" (but events can create many forms of coupling, especially if the nature of "these things need to change together" is not solved but the problem is baked into an even more distributed solution)
- Micro-services: "Separate services will solve this!" (but logical coupling remains, much like with events, things that need to change together will still need to change together, even if they are in different repos, sending messages over the network, and running in different runtime)
- Interface proliferation: "More Interfaces and APIs will help!" (feels right because APIs keep breaking, might even sound right according to concepts like the Interface Segregation Principle, but if two sides of the equation need to change, adding formality to the code in-between the two things will not defeat the change coupling)
- Theoretically pure domain boundaries: "We'll design the ideal business separations!" (and domain modeling does provide crucial insights into business complexity, but pursuing some final "correct" domain structure ignores that business reality constantly shifts. Teams reorganize, priorities evolve, and Conway's Law ensures that even the most carefully designed boundaries will be reshaped by the ever-changing reality of how people actually work together)
- Architecture pattern orthodoxy: "We'll architect it properly this time!" (but rigidly following pattern prescriptions without measuring their effectiveness against your actual change coupling and team dynamics often creates more coordination overhead than the problems they were meant to prevent)
Every single one of these techniques can help, but the devil lies in the details with each of them. These techniques also frequently become failed "big refactor" initiatives.
These techniques however are not why the projects fail. They fail because they ignore an important ingredient needed to guide decisions. Ignoring change coupling will almost always guarantee your project will fail to reach the goals everyone had hoped for (and often, with significant political capital tied up in these projects, high-level leaders have a lot to lose by going for the common technique without the change coupling nuance to go along with it).
How Change Coupling Acts as a Hidden Cost
Cognitive Load
The nature of a poorly change-coupled system is that the changes a developer needs to make are spread out and disjointed through the system, or even the size of the change is larger than it ought to be. This creates enormous cognitive load for developers. They must navigate changes across a much larger surface area, mentally juggling multiple unrelated systems. The complexity compounds as they track an ever-growing web of dependencies. It can easily happen that many of those regions are unfamiliar to the programmer and they must stop and learn how the small change they need to make in that system might be logically coupled to many other unanticipated systems.
This ever stretching web of facts exposing more webs of facts can lead to a form of change paralysis where it is too complex to consider which files must be changed and how. There are techniques to support with messy code refactoring work but they are time consuming and it is often better to avoid entering such a situation in the first place.
A poorly coupled system forces exponential discovery work on the developers as they work to correctly understand what sort of fix is needed, as well as the effort to make a change or fix. A poorly coupled system is therefore a massive non-required waste put on developer productivity and worth serious attention in the pursuit of increasing productivity.
To make this issue more material, consider two different projects making a similar logical change. In one where changes are cohesive, the area of change is focused and logical. In the other, they are scattered across the project and important parts are well hidden in seemingly unrelated files.
Cohesive Changes
Consolidated area of change
Project Structure
Non-cohesive Changes
Scattered through the project
Project Structure
When considering team dynamics, change coupling also plays a role. Depending on how work flows through your organisation it is possible that a simple change requires coordination of multiple teams. To give you an idea of how bad it can get, I have personally witnessed monoliths where one simple change required approvals from the team leads of 12 different teams. Safe to say, very little got shipped in that project before we changed the way teams worked.
Sometimes this manifests itself as each team that owns a physical "slice" of the app needing to coordinate the change in their own planning cycles (and the political will to get it into the cycle). Other times it requires their approval. Sometimes changes are made without the team that "owns" the affected code knowing about it and discovering an unwelcome surprise in their code in the future, or even breaking the change later on causing bugs because they didn't know the full context of what it was for.
Often when this situation persists long enough, the region of the code that is poorly coupled often becomes petrified like rock. Fewer and fewer people know how to correctly change the code. More people are truly concerned and afraid of touching it. Nobody takes accountability for it and it goes without ownership. Tragically, these are often some of the most business critical functions. And commonly, a development team unable to effectively change the heart of the business ends up nudging or even forcing the hand of business strategy to dilute their plans and expand around this calcified change bottleneck and look for revenues in other areas where they can get changes made faster.
Change Coupling Hide & Seek
Experience what it is like to work in both a well-coupled and poorly-coupled system.
Find all the functions that need to change together to implement a feature described in the ticket provided. Function names and comments should help you understand where changes need to be made, but sometimes changes go beyond the surface level.
- Click on the correct functions to earn Change Points, and make progress towards shipping!
- Wrong clicks add Extraneous Cognitive Load, which is a bad thing!
Choose Your Challenge:
Change Coupling Reveals Deeper Connections
Sometimes poor change coupling is hard to avoid or even intentionally chosen due to other benefits which makes this disadvantage worth it. Change Coupling commonly exists alongside other types of coupling such as logical, conceptual, temporal, or others, but the presence of them does not ensure change coupling either.
Here are a few quick examples of where poor change coupling can be the right trade-off:
- Conceptual coupling: Functions that seem unrelated but always change due to shared business rules for systems which do not have a common abstraction (or might not even be possible, such as a document generator and a calculator)
- Temporal coupling: Functions that must be updated in sequence even when physically separated, such as things like start-up and shut-down or clean-up code, or a series of database migrations.
- Regulatory coupling: Code bound together in change by compliance requirements. For example, GDPR requirements may force many unrelated regions of the code to store data in different ways.
Change Coupling is an evolutionary concept. It is hard to design a perfect system for change coupling and impossible to design one that is timeless and covers all cases. The ideal path is to aim for most changes your team will tend to make should be optimized well to keep the change coupling minimal and logical. Over time, the things that change will drift, and occasional individual changes will need to be cross-cutting through your software design, making them very different from the average change you need to make. However, when your typical change is cross-cutting, you have a clear signal that your team is struggling with change coupling.
Making Change Coupling Actionable for Your Team
Rule #1: Remember that everything is a trade-off and the goal is not to reach perfect decoupling.
Rule #2: Remember that these tools are meant to guide sense-making, not do all the thinking for you.
Step 1: Identify And Select Opportunities
- Read the analysis to discover which parts of the code tend to be the best and the worst. Does it match with your team's intuition about the code? Debate discrepancies.
- Look for which co-changing patterns are likely to be the most common in your project over the next 3-6 months. Do not go for the worst change patterns, aim for the ones that rob your team of time and brain cells (collectively).
- Make a short-list of the few most important change-coupling problems to solve. I often default to optimizing for time saved, but your team may wish to optimize for something else like reduced change risk, on-boarding experience of new team members, or something else entirely different.
Step 2: Coordinate and Plan Changes
Your team must form a technical strategy on how to change the code structure to avoid change coupling. This might involve tactics like merging code, splitting code, or moving parts of code to a new home. Often I find Dependency Inversion (mentioned earlier in this article) is a good option.
Sometimes it is valuable in terms of product discovery to have change-coupled code as a trade-off against learning about a deeply unknown domain. In this case, it would be worth looking at how to accelerate learning about your customers and domains as quickly as possible to reach a more stable understanding.
There are countless solutions here. None of them are perfect. Some of them are good enough to be called an improvement.
Many changes here can require cross-team alignment on changing code boundaries. If you have challenges explaining why to your peers, you would benefit from reading Framing Change: Why Good Ideas Go Nowhere.
Step 3: Commit to Regular Reviews
If you have a mature/standard architecture process this could fit in as a normal recurring agenda item every quarter. If not, you may need to carry the torch for a while or form an unofficial team that check in every few months.
Change coupling is tricky because it is not something that can be read in the code and often is only obvious in hindsight. This is because it is an ever-evolving sort of coupling that has as much to do with code as it does the business strategy and human cultural norms. Things that change together do so because they share a common reason to change, and they share a reason because humans came up with that reason. If reasons change, the coupling does too. Some bits of code will forever be deeply change coupled regardless of what humans decide, but many will not be. This is why it is so important to regularly check.