We have been told again and again that one of the golden rules of software engineering is the famous “Don’t Repeat Yourself“; if you find code that is repetitive you should try to abstract it, however, how does a modular architectural approach affect the DRY
principle?
Let’s imagine that you are building a new section of a modular frontend application, you have the freedom to release to production as many times as needed without needing intervention from other teams, however, you start noticing a pattern, some of the features you are building are so similar across all modules that you decide to abstract them into their own dependency.
What seems like a perfect application of the
DRY
principle quickly becomes a bottleneck.
Let me expand on that… if the code that you abstracted is rapidly changing and your main module’s functionality depends on it, you will have to ensure you deploy that code before it can be used in your module and instead of having the freedom of one deployment, now we have to deploy two different codebases to achieve a working feature.
Let’s add something else on top of it, the second team who wants to reuse the same library that you extracted have also added some “extra features” to adjust the library to their own use case and now you find yourself quickly maintaining more and more features that were not part of the original requirements.
Finally, we end up with larger codebases and more complex code to adjust for all edge cases or even worse we might end up with a smaller inferior solution because we had to settle for the minimum set of features that apply to both use cases but leaving personalised use cases behind.
Reusing is very difficult! in some cases the cost of reusing is higher than building the feature twice.
Three strikes… reuse!
Ok, so that’s if you have only 2 similar features, but what about the “Rule of three“, surely that is when you should think about an abstraction, right? well, like in most things in software development, it depends…
One of the main benefits of a modular “Micro-Frontend” architecture (and one of the most difficult to apply) is to decouple different modules to allow for independent deployments. If we start adding abstractions that break this pattern and tightly couple the different modules back together we end up back on square one and we negate this benefit resulting in a “distributed monolith”.
It’s all about the simple things
So hold on, are you saying that we must reinvent the wheel again and again?… Of course not! reuse is very useful when you try to abstract small, atomic and static things. Component libraries and design language systems are the best examples of how to reuse code effectively without breaking the freedoms of independent deployments… so don’t worry I am not suggesting that you should re-create the same button 100 times.
Reusability is a nice “side effect” not the target
Features and user experiences that are not static or atomic are very different when it comes to reusing code and not repeating yourself. It is definitely possible but harder and it might come with a cost. The recommendation is not to force the reuse and let it naturally occur, having a clear contract between the reused features and the consumers and preserving the ability to deploy independently to avoid blockers.
A great example of code reuse in a Micro-frontend application is the header and the footer; they are features that contain an entire user experience that is present in most of the pages of the website. After transitioning to independent deployments and using a vertical slice approach, each team might find themselves building and providing the same header and footer. This could lead to multiple copies that are not synchronized, affecting consistency and creating issues because there isn’t a centralised place to update or manage each copy.
The key to reusing these user experiences is to allow for horizontal slicing of the application where an individual team can deploy the header and footer independently and other teams will just consume them, with a clear contract for required communication (like showing the login or logout button depending on the user session for example).
Conclusion
Reusing features or entire user experiences is very difficult! When approached by the temptation of abstracting an entire user experience I usually ask myself the following questions:
- Is the feature something that doesn’t change very often?
- Is this feature present in more than three places? (Rule Of Three)
- Is the code made up of simple atomic things like components?
- Can the feature be owned by an independent team?
- Can I deploy the feature independently without tightly coupling it to other features?
If the answer to most of the questions above is “Yes”! then “Don’t Repeat Yourself!”
This article was inspired by the “Microfrontend Myths” webinar by Cam Jackson.