Change Coupling: Why One-Line Fixes Touch Ten Files | Knowledge Base
Pangram verdict · v3.3
We believe this text is mainly human-written, with some AI content.
AI likelihood · overall
HumanArticle text · 1,559 words · 5 segments analyzed
Trade-offs FirstChange 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 The Things That Change Together Should Live TogetherChange 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. MODIFY🔴 CONSIDER CONSOLIDATINGHigh Change, Low PhysicalCode that changes together but lives apart. These functions are telling you they want to be neighbors.Example:Authentication logic scattered across utils/, components/, and services/ that regularly changes together.MONITOR🟢 SYMETRICALLY COUPLEDHigh Change, High PhysicalCode that changes together and lives together. This more ideal. Your physical code layout is symetric to code changes.Example:Payment processing functions in the same module that consistently evolve together for new features.MONITOR🟢 SYMETRICALLY DECOUPLEDLow Change, Low PhysicalCode that rarely changes together and is not stored in the same place.
Clean separation that reflects actual independence.Example:Database connection utilities and UI components that evolve independently and stay in separate modules.MODIFY🔴 CONSIDER SPLITTINGLow Change, High PhysicalCode that lives together but rarely changes together. May be artificially coupled from historical decisions.Example:A 'utils' module containing unrelated helper functions that get tossed in whenever the team couldn't think of a better home for it.
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.