Deep Dive into the UI Utility Library: Hawk-eye
Transformative changes have marked Yellow.ai's recent journey in web development. Migration of our cloud app from Monolith to Micro-Frontends with a poly-repo approach, initiated last year, was driven by a dual mission: to reduce build times significantly and to empower individual teams with module-level autonomy.
However, this was not smooth sailing. Code duplication came up as a major problem. Reusing some basic functionalities like utility functions, constants, state management, and UI elements across different Micro-Frontend was threatening to eat up the benefits we desired. The problem led us to search for a remedy, an instrument that would facilitate the process of code sharing, make it efficient, and make the Micro-Frontend(MFE) architecture more unified. That is where Hawk-eye, our custom-built npm package, gets into the game.
This blog post will explain how optimizing code sharing through a custom library called Hawk-eye can improve our Micro-Frontend excellence.
Understanding the MFE Architecture
Evolution in web development has taken place through Micro-frontend architecture as it offers a different path to developing web applications. The basic principle of MFE architecture is to break down large, monolithic applications into self-sufficient components in the same way that microservices work at the backend. Such an innovative approach provides development teams with the independence and adaptability needed to meet the ever-changing needs of web development. Each Micro-Frontend can operate individually and can have a different frontend framework of choice, design standards, and functionality. This process not only simplifies the development procedures but also sets or creates an atmosphere or culture of innovation in which teams may have a choice of the right technologies or tools that are best suited for use with the chosen Micro-Frontend, leading to the ultimate success
This constitutes a fundamental aspect of our strategy at Yellow.ai. We decomposed our cloud application into Micro-Frontends named Channels, the Integration, Engage, and Insights channels with a poly-repo approach (Each MFE will have its repository). These Micro-Frontends are added up into a single container application called web-app, which serves as an entry point to our product suite. They are built in a modular fashion with React as a frontend framework meaning that individual teams can own a separate Micro-Frontend which increases the speed and allows customization based on the requirements.
MFE architecture at Yellow.ai
Code duplication challenges
Code duplication is one of the common issues in MFE architecture and it affects both development and maintenance in the long-term. Nevertheless, this problem becomes even greater when adopting the poly repo approach.
When the utility functions, routing, state management, or the user interface components that make up Micro-Frontends if not carefully planned and implemented results in code duplication. This in turn leads to needless reinventions of already available functions, eventually wasting resources. Lastly, all bug fixes as well as modifications involve a well-coordinated concurrent synchronization of different code bases leading to enhanced maintenance challenges. In a nutshell, this code duplication problem poses doubt on the anticipated benefits of MFEs, such as modularity and independence.
In our Micro-Frontend journey at Yellow.ai, adopting the poly repo approach within the MFE architecture resulted in code duplication of shared utilities, constants, and React hooks across various Micro-Frontends. The consequence of observing multiple versions of the same utilities across MFEs reached a point where maintaining consistency became challenging, leading to confusion among developers. Ultimately it ends in disrupting the planned development workflows. Hence, there should be an optimal way to organize the code sharing across the MFEs.
Code duplication challenges across MFEs at Yellow.ai
Ways to Solve the Challenges
While the goal is to streamline code sharing within Micro-Frontend architecture, it's crucial to maintain a balance between reusing code effectively and preserving the autonomy and agility of MFE teams. Here, we discuss two strategic approaches to address this challenge in detail
Share Code as a Library
Packing reusable code elements as libraries is one way to prevent code duplication. These shared functionalities are packaged as standalone libraries that can be distributed and consumed as needed, eliminating the need to duplicate common utilities, constants, or React hooks across several Micro-Frontends. Despite the potential diversity in frontend frameworks or technologies used by different Micro-Frontends, the common libraries are designed to be technology-agnostic. They encapsulate functionalities in a way that is independent of the specific tech stack of each Micro-Frontend. Development teams can easily access shared code without sacrificing their autonomy by incorporating these libraries as dependencies. This approach facilitates efficient and consistent maintenance, ensuring that shared functionalities remain consistent, even if individual Micro-Frontends use different technologies.
Sharing code as a library
Share Code as an Individual MFE
Making specialized Micro-Frontends that are only used to host shared code components is another strategy. The purpose of these Micro-Frontends is to act as a central repository for shared functionalities, which other Micro-Frontends can readily access. It's important to note that these specialized Micro-Frontends are designed to be technology-agnostic, ensuring compatibility with various frontend frameworks or technologies used across the application. This method reduces the amount of duplicate code by storing shared code in a specific Micro-Frontend and allowing it to be utilized across the application.
Sharing code as a MFE
How We Proceeded
In addressing the challenge of code duplication within our MFE architecture at Yellow.ai, we implemented a practice where we grouped reusable code into libraries. This choice was made after a thoughtful evaluation of the benefits and considerations of this approach over sharing code as an individual Micro-Frontend. Now, let’s discuss the particular reasons for our choice.
Maintainability and Version Control:
We obtain a powerful way to preserve and version-control the shared functions by wrapping them up as libraries. For a purpose, libraries allow for independent management and updating of the code components, thereby ensuring that the change that occurred in the shared library is also distributed consistently on all Micro-Frontends. This approach simplifies maintenance efforts, making it possible for an organization to follow up the updates in an easy way.
Development Workflow Integration:
Custom libraries seamlessly integrate with standard development workflows, enhancing the overall development experience. Including custom libraries in the development process makes it easier to update the shared functionalities.
Testing and Validation:
Custom libraries provide a convenient environment for testing and validation of shared functionalities. Development teams can test library components independently, ensuring they function as expected. This can streamline the testing process, leading to more robust and reliable shared code.
Unveiling Hawk-eye: A Tale of Code Harmony
Hawk-eye is the custom library that is crafted as Yellow.ai's utility companion for frontend apps, to combat issues with code duplication among Micro-Frontends. At its core, this "Util Package" is backed with a central repository designed to house common utility functions, constants, and hooks.
Key Features of Hawk-eye:
Unified Codebase Management: Orchestrate a unified codebase by consolidating utility functions, constants, and hooks. This ensures a streamlined development process and consistency across Micro-Frontends.
Effortless Code Reusability: Develop a culture of effective code reuse, cut down on redundancy, and create a centralized location for reusable code components to simplify development.
Efficient Maintenance Hub: This "Util Package" serves as a central hub, thereby simplifying the versioning and updates. This ensures that common functionalities evolve uniformly across Micro-Frontends, reducing the risk of inconsistencies.
Implementation
Hawk-eye 1.0 (A Milestone in Code Sharing Excellence)
Code collaboration was revolutionized by the first version of Hawk-eye 1.0. As Yellow.ai’s point of light, it has dedicated a host for JS utility, constants, and React custom hooks, promoting code sharing across MFEs.
However, Hawk-eye 1.0 was more than just an introduction – it promised that development teams would be equipped using unified and simplified practices. We quickly adopted Hawk-eye 1.0 as our utils of choice in Yellow across all MFEs of Yellow.ai.
Hawk-eye 1.0 Overview
Challenges and Evolution: A Transformative Journey
Nevertheless, Hawk-eye 1.0 had some limitations, particularly with React-specific peer dependencies that limited its adaptability for non-React frontend projects. This was the challenge that prompted us to seek a more flexible solution, beyond the domain of frameworks.
Hawk-eye 1.0 Challenges
It should be noted, however, that this was the turning point in the way we approached Hawk-eye. The pursuit for inclusion sparked an architectural shift in Hawk-eye, Which is dividing the package into two separate bundles backed with a mono-repo. This marked an important milestone in Hawk-eye’s journey towards greater adaptability and acceptance.
Hawk-eye 2.0: (Elevating Code Collaboration Mastery)
Hawk-eye 2.0 represents a deliberate step forward in our journey toward mastering effective code collaboration, that builds upon the foundation set by Hawk-eye 1.0. It's not just an update, either. Rather, it is a strategic evolution that addresses the issues raised by Hawk-eye 1.0, embracing a collaboration model that improves our shared coding while also aiming for increased modularity and adaptability.
Key Enhancements:
Distinct Packages for Precise Solutions
Hawk-eye 2.0 divides itself into two distinct packages.
"Hawk-eye-core": A powerhouse for JavaScript utilities, ensuring a robust foundation for common functionalities.
"Hawk-eye-react": Dedicated to React-specific custom hooks and utilities
This enforces a better separation of concerns, enhancing modularity and ensuring a more tailored approach to code sharing. The result is that the packages are versatile enough to use in both react and non-react frontend applications.
Distinct Hawk-eye Packages
Monorepo adoption
A crucial decision in Hawk-eye 2.0 is the adoption of a mono-repo structure over poly-repo. Monorepo helps in simplification of the development process, code sharing between packages, and collaborative development for packages. This selection allows smooth code sharing between “hawk-eye-core” and “hawk-eye-react”, creating a unified environment.
Monorepo with Lerna:
There are several monorepo tools available in the landscape including Nx, Lerna, Turborep, etc., which have distinct merits. However, we went with Lerna because of the following reasons
Tailored task pipeline with Nx:
A tailored task pipeline that aligned Lerna with Nx was effective in running the scripts in a controlled manner. This is even more true in the case of Hawk-Eye where the build order matters meaning hawk-eye-core has to run before hawk-eye-react because core is a dependency in the react project. This intricate control ensures that our development workflow is productive and predictable.Flexibility in Dependency Management and Structure:
Using Lerna’s flexibility, we created a unified dependency management approach. Consistency in dependency versions is ensured by the global sharing of common packages across React and Core whenever needed. This flexibility allows teams to customize the monorepo of Hawk-Eye based on its specific project needs.Tailored Deployment Solutions:
Lerna's adaptability extends to tailored deployment solutions like dependent/ independent versioning of packages. For Hawk-Eye 2.0, this means the freedom to implement custom deployment strategies. Currently, we're utilizing independent versioning for packages, ensuring precise control over version management.
Moreover, Lerna was also among the most popular and commonly used package managers for building as well as publishing multiple JS and TS packages from one repository. Collectively, this brings Lerna best suited with the Hawk-eye monorepo architecture.
Nexus Repository: Streamlining Package Management
In both versions of Hawk-eye, the Nexus Repository acts as a centralized powerhouse for efficient package management. Serving as a unified hub, Nexus ensures streamlined storage and retrieval, offering developers a centralized location for package management.
Hawk-eye 2.0 Overview
Learnings from the Hawk-eye Journey
Setting out on the Hawk-eye path to Micro-Frontend excellence reveals a wealth of new information and important lessons. A few important conclusions come to light as we examine the nuances of package management, code sharing, and the transition from Hawk-eye 1.0 to 2.0.
Strategic Code Sharing:
Discover ways of sharing code in MFE architecture and understand the significance of code sharing. The way Hawk-eye's packaging and modular methods changed how we handle shared components in Micro-Frontends, making things simpler and better.Monorepo Mastery with Lerna:
Find the advantages of using a mono-repo setup backed by Lerna. Explore how this strategy enhances package management and version consistency and promotes modularity and collaborations.Centralized Package Management with Nexus:
Discover the ways to use Nexus as a centralized repository. See how it enhances the package discoverability, version control, and the singular strategy behind storage and retrieval.Versatility Beyond React:
Explore the versatility of Hawk-eye in catering not only to React applications but also seamlessly integrating with non-React projects. Witness how this flexibility opens doors to a wider spectrum of development scenarios.Adapting to Challenges:
Understand the challenges encountered in Hawk-eye 1.0 and the improvements made in Hawk-eye 2.0. Going beyond React-specific dependencies, the shift to a more inclusive architecture provides key lessons in tailoring solutions for changing requirements.
Final Notes
Hawk-eye is a game-changing solution for Yellow.ai’s Micro-Frontend architecture, reducing issues such as code duplication and improving development process efficiency. Our journey, which began with Hawk-eye 1.0 and continued with the upgraded Hawk-eye 2.0, is a dedication to improving Micro-Frontend development excellence.
The adoption of a mono-repo structure, orchestrated by Lerna and Nexus, not only streamlines package management and ensures version consistency but also serves as a beacon for collaborative workflows. Importantly, the modular separation of packages within Hawk-eye enables seamless consumption in both React and non-React applications. Development teams are empowered by this versatility, which provides a unified and flexible solution for Micro-Frontend excellence across various projects and frameworks. Hawk-eye is a testament to innovation, enabling teams to navigate the intricacies of modern web development with confidence and agility. Beyond just a case study; it's a source of inspiration, encouraging a mindset of continuous learning and refinement in Micro-Frontend development practices.
References
https://levelup.gitconnected.com/micro-frontends-what-why-and-how-bf61f1f0a729
Micro Frontends: A Deep Dive into Pros and Cons
The Dilemma of Code Reuse in Microservices