Framework Coupling Revisited

The first post that ever appeared on this blog was named “Framework Coupling”. In the post, I laid out a vision of evil frameworks that will destroy your project, your company, your career etc. The catastrophical tone of that post was a result of my fascination with Uncle Bob’s teaching and strict dependency management in general. Now, after eight months have passed, my views have changed a lot, this blog has changed a lot and I think it’s the right time to take another look at the topic.

What is Framework Coupling?

Framework Coupling is the degree to which your application is bound to the framework / technology stack that you’re using. It would be at it’s lowest when all the code related to using the framework would reside in separate files and the application would have no knowledge of the framework being used. On the other side, highest degree of Framework Coupling would mean that almost entire application logic is written using framework’s capabilities and if references to the framework were removed, the remaining code would not make any sense at all.

At the lower level, Framework Coupling appears as direct usage of framework’s APIs, using framework’s annotations, launching the framework in unit tests and so on. Using Framework’s naming conventions can also be considered Framework Coupling, although it does not alter your application’s behaviour (hopefully).

Consequences of (High) Framework Coupling

High Framework Coupling, as defined above, comes with important consequences. Let’s go through them one by one.

Shotgun Version Updates

(I named this consequence after Shotgun Surgery code smell.)

Whenever you want to use a newer version of the framework, you’re exposed to risk of modifying multiple source files, because of retired or deprecated APIs. In fine cases, the modifications are as straightforward as fixing the imports or changing used class / method names. In severe cases, the whole philosophy behind a feature might have changed and you might end up reimplementing big chunks of your application.

Induced Fragility

This is a directly related of the previous one. Whenever we update our project to use a newer framework’s version, we’re at risk of breaking existing application features. Such problems are most likely to surface when the tests are referencing the framework and we’re updating to a new major version. In such case, both the tests and the production code might stop working, which leaves us with no safety net. Another type of problem might arise (and not be immediately spotted) when we rely on framework’s undocumented behaviour. Such stuff can be subject to change even if the public API remains untouched. In this case, we’re left at the mercy of our test suite.

Feature Deprecation

This is the last one of version-related consequences. Some feature that we found crucial for our application’s well-being, might be deprecated or completely removed from future framework’s version. This would imply at least rewriting the future and, at worst, rewriting the application. In general, it’s important to understand that framework designers care about being cutting-edge and encouraging more people to use their creation. This might not go hand in hand with our application’s needs.

Test Impairment

When working with any code, that has been already deployed (as a jar or so), we cannot change it according to our testing needs, thus we must comply. The situation is no different when working with frameworks. As test-friendly as the framework is, as easy it will be to write tests for our application’s code. Mocking tools are nowadays extremely powerful, so simulating framework’s behaviour should not be a problem. Things are not as good when it comes to testing time and configuration. In heavily framework-coupled applications, a lot of tests require spinning up the whole framework machinery, which can dramatically slow testing time and put extra configuration burden on developers.

Framework Lock-in

This should be pretty obvious. Too much coupling with a framework might mean that we’ll never be able to change it, because that would take huge amounts of time and put us at risk of breaking the currently working code. Another thing that might fall under “lock-in” might be inability to use certain libraries or their version, because of their framework incompatibility. In that case, we might be left with worse or less-suited framework-compatible replacements.

Framework-Driven Design

When you take a look over a framework-coupled system, you’ll have no problems recognizing technologies and conventions being used, but the system’s heart, the business problems that it’s supposed to solve might be completely opaque. This might indicate suboptimal design decisions, because the domain has been fit into the framework standards, instead of the framework being used for the purpose of the domain.

Enough disadvantages. If things were so bad, nobody would use frameworks. Let’s look at the bright side.

Idiomatic Framework Usage

A lot of common problems have idiomatic solutions prepared either be the framework designers or by it’s users, based on experience. By abandoning the dreams of fully framework-independent application logic, we’re opening ourselves for the benefits of those solutions – wide adoption, simplicity, lots of tutorials and available community support.

Direct Design

As we’ll see in a future post, actively avoiding framework coupling can introduce a lot of indirections to the project. Although they might seem beneficial, as a form of abstraction, they may also cause confusion and force people to dig through extra source files to find out what’s really happening. Using the framework directly solves the problem.

Less Tedious Coding

Wrapping every framework service and mapping each framework data structure to our own can be a boring, error prone task. Coupling to the framework in some places can free us from the burden of excessive wrapping and mapping.

I could probably think of more, but the general picture should be clear at this point. We’re basically trading purity and safety of our application classes for simple and direct solutions available for the framework being used.

Scale of the Problem

When talking about Framework Coupling and possible negative consequences, it’s important to realize the impact range of those. In the original text, I stated that “Microservices don’t change anything”, because of “no difference between having 1 huge poorly designed application and having 20 small poorly designed applications”. While the argument itself makes sense, we need to realize that a poorly designed monolith and a poorly designed microservice can look completely different.

Framework Coupling and Monoliths

In case of a monolith, each of the consequences of Framework Coupling will span across a lot of code. The person upading the framework’s version does not only update it for his/her part of the project, he/she updates it for the entire thing. This means that multiple teams might have to be involved at the same time in updating framework’s version. Also, the bigger the system is, the more tests there are and the more complex configurations have to be done for testing purposes. Therefore, the impact of slower testing time can be meaningful, often way above acceptable. As for “Framework Lock-in”, dependency management in big systems can be quite a challenge, because every part of the system might have different requirements and, in different parts of the system, similar requirements may be fulfilled in different ways. Adding a heavy framework to the pile almost certainly won’t help.

Framework Coupling and Microservices

Almost all the negative consequences of Framework Coupling might become less significant when dealing with microservices. There’s less code to update, less testing to be slown down and smaller cost of rewriting significant portions of the application. There seems to be an interesting tension between first five consequences and the last one – the bigger the microservice is, the more burden will come due to the first five; the smaller the microservice is, the less domain logic it contains, which might then appear insignificant comparing to other classes. Obviously, we have to multiply the (smaller) problems by the amount of microservices. Therefore, I’d be particularly careful about things that might force me to “roll over” all my application’s codebases, whatever these might be.

What’s next

The text got a bit lengthy, so we’ll stop there for now. In the next post about Framework Coupling (not necessarily the very next post on Tidy Java ;)), we’ll have a look at techniques to limit it’s impact and find the sweet spot when working with frameworks.

About the Author Grzegorz Ziemoński

King of Tidy Java, nerd that thinks about producing perfect software all the time and proud owner of 2 cats.

follow me on:
  • jonniesweb

    Interesting post! The great thing about microservices is that they can independently change their needs with time. The last section mentions that a bad framework choice would require rolling over all codebases to a new framework, if I understood that correctly.

    In my mind I would live with bad framework choices in all of my microservices. Only if I’m actively developing a microservice and the bad framework is affecting me enough that it is worthy of my time to replace, will I then replace it.

    What do you think about living with bad design/framework choices?

    • Hey! Thanks for the comment and sorry for the late reply, I’m extremely busy last days.
      About rolling over codebases, I meant something like.. the framework uses some fancy/custom protocol and then a version upgrade is not backwards compatible (that’s a stupid example and that’s why I wrote “whatever these might be” in the text). Then you’re either stuck with the version or you upgrade everything at once.
      As for living with bad choices – I think it’s a matter of cold calculation: how much value do we get against how much we invest in some reasonable timeframe (which will depend on the project). The same applies to upgrading frameworks, JDK versions etc.