How To Avoid Disaster By Mitigating Coupled Concerns

“Tight coupling” is where logic centered around doing a thing is spread across multiple places, which is never good for long term code maintenance. Tightly coupled code is not easy to change, hard to follow, and has a higher risk of regressive bugs. Promoting loosely coupled concerns in a design is a strong hallmark of a good engineer. A codebase with decoupled systems is a system that is sustainable for maintenance and growth.

The “S” in SOLID (an acronym for the object-oriented (OO) standard to help decrease coupling) is for the Single Responsibility Principle (SRP). This principle says to keep classes (but we’ll expand that to code blocks) isolated to a single responsibility, also known as a “concern.”

Isolating concerns gets a bit tricky because they can be tough to identify. SRP is key to helping a codebase be extensible; it is worth spending the time enforcing this principle so that you can identify when a design is tightly coupled and how to decouple it.

The dangers of not promoting loosely coupled concerns

The danger of failing to catch concern coupling is that it starts you down a slippery slope of exchanging velocity for technical debt. Engineers focus on getting code out that does the job over code that works in a concern organizing fashion. Over time, a little bit here, and a little bit there, until you’ve created a tightly coupled monolith. Logical units will explode with so many mixed concerns that new contributors may be lost or make fundamental mistakes that lead to regressing a concern that was not apparent from the change.

We have all been there and have seen this to some degree. We’ve perhaps even contributed to these leaning towers hoping we don’t give it the final nudge. When asked to add additional features to the monstrosity, the team will panic. It’s an uphill march to ensure no expectations end up regressing with any business change needed. In the end, the coupling all started with a small code block that did not have a clear definition of concerns and a will to separate them.

Once code becomes too tightly coupled, code velocity will suffer, regressions will happen when unintentional expectations change, and developers avoid touching the coupled code. War stories will be told about the great battles fought, introducing even the most minor of changes. Ultimately a team will need to refactor or rewrite. Both can be risky endeavors that will cost much more in engineering time than the upfront commitment it would have taken to keep things loosely coupled at the start.

Identifying code that does not couple with different concerns

You will know a system that has done a decent job of separating concerns when the code is highly readable. Please read here for my thoughts on improving code readability. Above all, the easier a design is to read and understand, the easier it is for other engineers to adopt and continue to build on top of it.

Decoupled code is composed of simple to follow systems and patterns that make themselves easy to understand. Furthermore, code forms natural “boundaries” or “layers” using semantics, nomenclature, or directories to distinguish themselves easily from other systems. These layers clearly communicate to engineers contributing to the code on where specific concerns should live.

Examples of coupled and decoupled concerns

Imagine building a UI component for a web front-end using Javascript and React. A design that is tightly coupled will place all the logic into a single .jsx file. A JSX file should distinguish UI code specific to rendering in React. Business logic is logic that defines why a UI exists and expectations of your business product. UI-centric software needs to retrieve data displayed, transform the data with business logic, and then render. Mashing all of this into a JSX file with the concern for rendering is a classic disaster.

An improvement to favor clean decoupling between data, business logic, and UI rendering is to split these into tiers (or layers). Let’s use the classic N-tier architecture for this example and create a 3-tier separation. We could just as easily use MVC (Model-View-Controller) as well.

Data retrieval and model transformation occurs habitually in the “data layer.” Then, data flows through to the “view model layer.” The view model layer is where business logic such as data clean up, calculations, additive logic from the business side tends to live. The view model exposes an API of structs, lists, and properties to the UI to bind. Then, take the “rendering layer” consisting of React, JSX, styling, and concerns related to how things look, animate, or user input.

This 3-tier example is a traditional design that gives a clear definition of where concerns live. This separation helps prevent coupling more than the monolithic approach mentioned earlier.

Cross-cutting concerns

Global system-wide necessities become unavoidable couplings. These are “cross-cutting concerns.” There are ways to mitigate cross-cutting concerns in the design, but they usually cannot be eliminated. The best you can do is reduce their implementation exposure through your codebase. For more on cross-cutting concerns, I share my opinions on that here. A quick example of an unavoidable global cross-cutting concern is usually logging.

Cross-cutting concerns are best wrapped in abstraction and not directly exposed. Otherwise, I’ve seen cross-cutting concerns become a mess in many large projects requiring large refactors to untangle them. Changing an unwrapped cross-cutting concern can require a lot of engineering time and effort.

Guidelines to help promote loose coupling of concerns

  • Single Responsibility Principle. Keep all functions, modules, and layers committed to doing one thing.
  • Keep your code readable. Readable code will communicate the one thing that the code, module, or layer is responsible for.
  • Don’t tie to concrete implementations of cross-coupling concerns. Identify system-wide concerns and provide a meaningful way to wrap them and isolate those concerns. That gives you power over changing those implementations and extending them as needed without large changes.
  • Keep your code concise. Long code is going to allow multiple concerns to leak in and some form of coupling. Keeping a habit of brevity and conciseness helps enforce code decoupling as it makes a forcing function to make your code more modular.
  • Identify the concern with a unit of code. From transforming data, rendering UI, or dealing with business logic, understanding the concerns is a strong step to ensure concerns are in the correct place. The relative concern of code needs to be easily understood by readers.
  • Enforce strong patterns. Over time, patterns will become apparent in the codebase and encourage all engineers to hold to the pattern. A clear and concise pattern that reads well will lead engineers to enforce the design naturally.

Keeping concerns loosely coupled with simple to follow patterns

In conclusion, cross-cutting concerns are by far the most devastating thing that will make a codebase sink over time. Failing to do preventative design to decouple core components early on in your software design will come back to haunt you. Over time, it will lead to technical debt that eventually becomes painful reactors or re-writes.

It doesn’t cost much extra engineering effort or time to separate concerns. The key deciding factor is the engineer and the engineering team’s skill and experience to identify them effectively.