Therefore, there is a tremendous potential payoff for organizations that get good at supporting constant change in their UIs. The principles of evolutionary architecture can be applied to UI development to great advantage — for example, certain architectural patterns, like micro frontends, can be used to ease the process of migrating while keeping your core business functionality protected.
To understand what this looks like in practice, let’s examine the case of AngularJS, the frontend technology that we have most frequently helped clients migrate away from in recent years. We’ll reflect on our long experience of migrating legacy UIs more generally, while using AngularJS as a specific example to guide our thinking. Whether you are currently on AngularJS or another UI framework, you’ll come away from this article a little more prepared to modernize your UI.
The AngularJS end-of-life
AngularJS is a component-based framework for creating client-side web applications. It is maintained by Google and was first introduced in 2010. The framework hit an early road bump in the first major version upgrade, which introduced so many breaking changes that it is generally considered to be an entirely separate framework. This second version, known simply as Angular, is still widely used to this day.
On December 31, 2021, the original AngularJS will reach its end-of-life. This is a momentous moment for the frontend world, not only because it marks the sunset of a popular and influential UI framework, but also because there are plenty of businesses still on AngularJS that are only now beginning the process of moving to a more modern framework. We have seen a steady stream of work come in from clients asking for our help in migrating away from AngularJS for years, and we fully expect this trend to continue as we approach the end-of-life.
At Thoughtworks, we have seen and used two main approaches for migrating UI code: Martin Fowler’s classic strangler fig pattern, and the cruder and more risky big bang rewrite. As we will see, both have a place in your toolkit, and we have applied both to AngularJS migrations.
For large, legacy frontend applications, we use the strangler fig pattern as a catalyst to think about coupling and functional cohesion in our applications.
The strangler fig migration
The strangler fig is a popular design pattern used to gradually create a new system around the edges of the old, letting it grow over time until the old system is eventually completely replaced. In the UI space, we use this pattern to introduce a new feature or redesign an old one with a different, often more modern, technology from the one currently in use. This approach allows teams to continue delivering value and features to the final users while gradually replacing part or whole of a legacy monolithic UI application.
For large, legacy frontend applications, we use the strangler fig pattern as a catalyst to think about coupling and functional cohesion in our applications. This then allows us to achieve architectures which better support future change — for example, by modularizing a monolithic UI, or even by breaking up the monolith entirely into well-bounded micro frontends. This allows teams to start thinking of their application as a collection of different business capabilities (bounded contexts) that can be worked or reworked on independently.
Of course, the gradual migration approach is not a silver bullet. Potential challenges include user experience degradations resulting from combining multiple UI frameworks on one page (network performance and CPU load), as well as from UX inconsistencies between the old and new worlds. Developers’ cognitive load and efficiency may also be negatively affected due to the coexistence of two tech stacks, and the scaffolding and interface mechanism necessary to support them.
Such issues can be mitigated by introducing techniques like code splitting and lazy loading, and setting up appropriate governance to enforce practices that improve performance. UI guilds and dedicated component library teams can also help factor out duplicate work into common base components and ensure that UX consistency is maintained between teams.
Case study: German real estate platform
Back in 2017, Thoughtworks was helping out one of the biggest real estate platforms in Germany with delivery. Our team was responsible for development and maintenance of the two most visited pages of the whole company: the search and the listing page.
When Thoughtworks joined, the client had already migrated the search page from JSP and jQuery to a more interactive server-side-rendered React application through a big bang rewrite. It took them around one year to rewrite everything in the new framework, preventing the team from delivering significant business value for their customers during the migration period.
It was now time for their listing page to go through a significant redesign. The listing page was a JSP and AngularJS hybrid application serving millions of unique visitors each week. One of the first things we had to discuss was how to conduct the redesign: a big bang rewrite was out of the question this time due to the client’s bad experience with the search page. An important question that needed to be answered was whether we should perform the redesign using the current technologies or gradually introduce a more modern technology stack. We decided to go with the latter option, and after evaluating a number of technologies (including modern Angular), opted to migrate gradually to React.
KPIs and fitness functions
When you have millions of users visiting an application you want to make sure that whatever changes you are planning to release don’t impact your KPIs significantly. You need to have infrastructure in place that makes tasks such as gradual rollouts and A/B testing relatively straightforward. You can then write fitness functions against your KPIs (if you don’t have them already) and easily compare and contrast results from the old and new apps.
The first test we conducted to determine the feasibility of our approach was to expose a small percentage of the users to a version of the application loading React and AngularJS runtimes simultaneously. This gave us an insight about how the two frameworks performed together in the same browser. Luckily for us it turned out that KPIs were not significantly affected by introducing React into the listing page; this was the green light to move forward with a gradual redesign using React. Of course, your mileage may vary depending on your KPIs, which is why it is imperative to identify robust fitness functions for your business and product.
The first challenge we faced when we started the gradual migration was an outdated and unmaintained Maven-based frontend build tool. Modern UI frameworks are often utilised in conjunction with Node.js based dev environments (e.g. Create React App, Vue CLI, etc), which abstract away much of the complexity of frontend tooling (e.g. Webpack, Babel, etc). Given the hybrid nature of our AngularJS/React solution we could not leverage a pre-made dev environment and instead had to manually configure our tools. This type of work often requires support from someone with specialized knowledge in frontend operations.
We knew from the beginning that we would not have the resources to perform a full migration to React and that the two frameworks would have to live together for a while. Once we carved out the relevant part of the application which needed to be redesigned we started asking ourselves two important questions: “How do we keep the branding consistent across the two different applications?” and “How do we pass information from one app to the other one so that the customer continues to get a seamless experience?”
The answer to the first question was relatively straightforward for us since the company had a custom-built CSS framework, comparable to Bootstrap, which was consumable by both frameworks without problems. The second question was a little trickier, but we made sure to carefully identify a relatively self-contained bounded context for our redesign, which required little communication with the rest of the application. Inspired by the loose coupling achievable with micro frontend architectures, we decided to handle the cross-framework communication via simple native custom events.
We successfully A/B tested and then gradually released all the redesigned parts over a period of six months. We managed to deliver value continuously to the final users — at the cost of an application which was now built with two different UI frameworks. Before focusing our efforts elsewhere, the team documented a high level roadmap to gradually continue the migration over time. Today there are still some parts of the application running in AngularJS, but the bulk of the application, including the main router, uses React.
One of the risks to keep in mind with gradual migration approaches is the possibility of never entirely finishing the migration. This can happen for a number of reasons including a shift of priorities or a budget cut. Our general advice to mitigate such risks is to organize the migration in a set of small independent steps which incrementally bring you closer to the desired outcome. As any of these steps could be the last one the team may be able to take, each step must leave the application in an acceptable state that is at the very least not worse than the initial state.
Finally, we want to highlight that there are cases where it’s absolutely fine to keep part of the application running on an old technology, such when there is a bounded context that is well-defined, stable, and sees little to no active development.
The big bang migration
The simpler and less elegant alternative to the strangler fig pattern is the big bang rewrite. This is exactly what it sounds like and typically involves stopping development of new features while you rebuild your product from scratch.
This approach is generally understood to be an antipattern, and for good reason. Here, the risk of project cancellation rears its ugly head in a different way. If your budget is cut halfway through a strangler fig migration, you at least have something to show for it, especially if you break it down into workable middle states, as described above. If a big bang migration is cancelled, there is a risk of the entire effort being wasted, since it is by definition difficult to continuously deploy to production with this approach.
However, while use cases are rare, we have found that the big bang rewrite does have its place in the frontend legacy migration toolkit. Whether or not this approach makes sense for you ultimately boils down to the questions of how much code you already have, what state it is in, and who it’s for.
Case study: US-based international airline
In late 2018, Thoughtworks was called in to rescue a business-critical project for a well-known US airline. We were engaged to replace the existing AngularJS app with a modern alternative while improving overall quality, delivering new features, and meeting the aggressive business timeline.
We performed a code assessment and found that the code didn’t meet our general standards for quality and had barely any automated tests. In this situation, given the high delivery pressure and the general sensitivity of the aviation industry to engineering quality, we couldn’t recommend the strangler fig approach. It would have taken too long to set up a gradual migration path while assuring the quality of the untested old code. At the same time, the UI codebase was comparatively small, which made it less painful to rewrite from scratch. Furthermore, the product was built for internal customers only and was not yet in production, which gave us more leeway to pause feature development during the rewrite.
We settled on a big bang migration to React. We onboarded a ten-person team that focused on delivering the UI in January, and by April we had surpassed feature parity with the old application and delivered an MVP. This success would have been difficult for us to replicate at our German real estate client, given their different scope, constraints, and challenges.
If you have comparatively little code of comparatively bad quality, and especially if there are no expectations of existing users to manage, then a full rewrite may be a good option for you. Or in other words, while big bangs might be a no-go, a “small bang” migration may be an appropriate choice for a legacy UI application.
Safeguard your business-critical functionality
UI legacy migrations are tricky, yet can unlock critical business benefits. The good news is that the frontend ecosystem really has stabilized and matured in the last half a decade. However, change will still come. Staying on the cutting edge of UI and meeting users’ ever-increasing expectations will at some point require you to be able to shift your technical approach, whether this involves migrating away from your current framework, refactoring base components into a component library, or introducing micro frontends. For an organization to stay nimble and competitive, it can’t put all its eggs into one basket.
When you do find yourself needing to migrate, think carefully about your situation and be realistic about what can be achieved. In our experience, you will almost certainly want to choose some sort of gradual strangler fig approach, where never fully completing the migration is a realistic possibility. A big bang rewrite may be a viable alternative in specialized cases where the existing codebase is small and of particularly low quality.
Whichever approach you choose, identify your core UI KPIs and protect them using fitness functions. Once these are in place, your business-crucial functionality will be safeguarded, and your organization will be ready to weather any storm that the technology world throws at it.