Lego Building Blocks
Lego Building Blocks

Avoid Tight Coupling With Cross-Cutting Concerns That Will Erode Projects

Cross-cutting concerns are essential to identify and isolate as your application grows. Failure to do so could lead to feeling you will have to commit to an expensive rewrite of your application later. Avoid tight coupling with cross-cutting concerns that will erode projects by identifying and isolating these concerns ahead of time.

This is a part of engineering by Separation Of Concerns (SOC) where not all concerns can (or should be separated). However, by using good design practice, you can abstract these global concerns out of your core business logic.

What is a cross-cutting concern?

Cross-cutting concerns are typical “utilities” or framework-level pieces that you utilize throughout rendering, state, and business logic (this is what makes it cross-cutting). A typical example of such would be:

  • Logging
  • Telemetry
  • Storage
  • Error Handling
  • Caching
  • Authentication
  • Security
  • Feature Toggles
  • Localization

There is likely more, but this should start sounding familiar. Let’s take the below code sample and identify the cross-cutting concerns.

class CoolFeature {
  public constructor(private coolApiFramework) {
    console.log("We've constructed!");
  }

  public async getTheData(): Promise<any> {
    const payload: string = this.coolApiFramework.getPayload();

    const data = await fetch("http://someCoolRestApi.dot/stuff", { body: payload }).then(
      response => response.json()
    );
    localStorage.setItem("someKey", data);
  }
}

If you’ve guessed that the cross-cutting concerns are using console.log(), the fetch API, and localStorage API all in one place; then you are correct! These APIs are built natively into most web browsers. While they may seem “safe” for most front end web clients, they might someday be deprecated or no longer meet your performance needs. Thus, if you rely on them directly, you will find yourself pretty “tightly” coupled here.

How To Avoid Cross-Cutting Concerns Getting Unruly

Avoid Tight Coupling With Cross-Cutting Concerns

By using a little bit of abstraction, you can prevent this tight coupling from the very start. The looser the design is from coupling; the more quickly you will be able to slap new implementations in for cross-cutting concerns, and have the freedom to use them throughout the layers of your architecture! In other words, you’ve gone the wrong path if implementation changes end up being many files large. (This is a real sign of tight coupling).

For example:

class CoolFeature {
  public constructor(private coolApiFramework, private core) {
    core.getLogger().log("We've constructed!");
  }

  public async getTheData(): Promise {
    const payload: string = this.coolApiFramework.getPayload();
    const dataClient = this.core.getDataClient();
    const storageClient= this.core.getStorage();

    const data = await dataClient.get("http://someCoolRestApi.dot/stuff", { body: payload });
    storageClient.set("someKey", data);
  }
}

What we did here is abstract the cross-cutting implementations behind an exposed API. Instead, we access directly through the object named “core.” The caller does not need to care that behind the scenes storageClient is just using localStorage (indexDb or whatever), and that dataClient is just “wrapping” fetch. What matters is that these abstractions always fulfill their API contracts in any environment (Node.js, service worker, different browsers, etc.). The abstraction allows the code-base to scale over time and enable development teams to swap in different implementations that use the latest technologies or environments. (Or prototype and experiment with various things without touching many other files.)

Therefore, when it comes down to deciding on rewriting bits of your stack; this pattern will make it much easier to modularize your cross-cutting concerns and re-use them.

Design Patterns Matter

The code above utilizes a wrapper design pattern around the implementation providers and exposed to the core. A factory design pattern is great to generate a core with a run-time config and environment detection. The beauty of this pattern is that it makes mocking out the core APIs much less costly. Since you can make a full mock core implementation and pass that around throughout your unit tests and end-to-end tests.

Let me know if you’d like me to dig deeper into any of the concepts this article covers! If you enjoyed this article, be sure to keep an eye out for future architecture articles. Go forth and be sure to identify and avoid coupling with cross cutting-concerns on any projects your working on!