Rushing to meet deadlines or other outside circumstances can sometimes result in less-than-perfect code quality. Met with a major app redesign, we strategically rewrote and refactored our code, transforming our ugly duckling into a beautiful swan.
Many digital products available today started out as small, innocent MVP projects. Sometimes, getting that app out into the world is the main concern, and getting it there quickly matters more than its internal structure, architecture, or other constituent parts that become important as the product grows into a large, multi-screen complexity.
The reality is, the best architecture in the world can’t save you if someone beats you to the punch with your app’s core idea.
Developing a piece of software doesn’t happen in a vacuum, and outside factors often dictate the tempo and feature prioritization, as we’ve had it happen on one of our projects. After years of ongoing work and feature development, we started noticing slowdowns in development as well as other issues. Small fixes started to take longer and longer, and we weren’t happy with our efficiency on the project.
Then a new, large change request came to us, and we knew we had to do something about our legacy code. It was time for a complete overhaul.
The road to technical debt is paved with good intentions
The project, a mobile banking app for a major client, was around five years in the making at the time. During that period, we added numerous features to the product, both large and small.
Taking into consideration the project’s initial scope and timeline, and the fact that Swift was still in its early stages, we decided to go with good ol’ Objective-C. Our architecture of choice was a simplified version of VIPER, basically VIPER without a Router and an Interactor. Since time was of issue, we also couldn’t build every little thing from scratch and had to compromise by using some third-party libraries.
As you can see, it was the perfect breeding ground for a future escalation. I should note that we did do some refactoring and maintenance along the road, as much as time and budget allowed us. First, we switched to the latest version of VIPER, added some new screens using Swift, and adopted a reactive approach to using RxSwift. We also removed almost 70% of the third-party libraries we added initially and replaced them with our own logic.
After five years, it was time for a major app redesign. Designers poured hours of work into it, and the result was a shiny new look made up of a number of reusable components. However, our codebase wouldn’t be able to handle it easily.
We knew we wanted to go with the atomic design pattern, but were painfully aware that more than half of our original code could not support this.
It was time to decide if we wanted to “hack” our current codebase in some way, which would likely give future us more issues to handle down the line, or invest a little more time and perform a more serious intervention.
Things were quite clear – we had a large technical debt to resolve ASAP.
To refactor or to rewrite?
Code refactoring and code rewriting are two buzzwords that are frequently thrown around when discussing how to deal with legacy code. However, they are quite different in their nature.
Refactoring is a process of changing smaller chunks of code over a period of time while leaving the original logic mostly intact. It is usually done on a smaller scale to keep the codebase easily readable, decrease the number of bugs and their likelihood to occur, improve app performance, etc. Refactoring usually doesn’t take as much time and resources and can be done periodically whenever software developers do something in the module or add new features.
While refactoring changes small bits of code, rewriting replaces the entire code. After a rewrite, you have brand new code, just like you’d write a screen or a feature from scratch.
Code rewrites are usually done only when you have to change more than two-thirds of the code or replace it entirely. In our case, we had to replace Objective-C code with Swift, two programming languages that couldn’t be more different (apart from the fact that they use the same frameworks provided by Apple).
Clearly, a complete rewrite takes far more time and resources than a simple refactoring. However, as the old code base full of issues, bugs, and technical debt is removed, we give that module a clean slate.
For us, both time and resources were a challenge. Whichever approach we chose, we knew that we couldn’t stop an active project so that we could prepare the app for its redesign.
So, which approach did we choose? Both!
Time to refactor and rewrite code!
We added a few additional software engineers to the project and had a development team ready to transform this ugly duckling into the beautiful swan it is today.
As mentioned before, more than 80% of the code was not ready for a major redesign since it was largely written in Objective-C and related to the screens we had at the time.
Our plan was to rewrite the code for the redesigned screens while refactoring some managers, helpers, and formatters to support the new code (and to do a little code cleanup!).
Several team members were responsible for rewriting and refactoring the existing code and redesigning the app using the latest principles and technologies we now use as a standard on all our modern projects.
At the same time, other team members continued working on new features and bug fixes. Both teams used the same approaches and technologies we agreed on – (Rx)Swift, and the latest version of VIPER.
The rewrite (redesign) part was done on a separate branch, while refactoring was done on the main branch. We split the rewrite into two major releases. The first release covered around 70% of the app. The second release followed the first one by a few months and covered the remaining 30% of the app.
As for the new features, the only difference for those two teams was the user interface. While the “rewrite” team used newly created components for the app’s (atomic) redesign, the “maintenance” team worked with the old screens, performing maintenance and adding new features to the “main” branch. As a feature was completed, it was merged on the redesigned branch. That way, the business logic didn’t have to be written more than once, as the “rewrite” team only needed to change the UI.
This approach sometimes led to double UI interface creation (two different Views in the project), but that was the price we were willing to pay to speed up the whole process.
The result? Quite impressive, thank you for asking
A few months later, we basically had a new project – our beautiful swan. The app became beautiful on the outside, and more importantly, on the inside as well.
Outer beauty turns the head, but inner beauty turns the heart.
Finally, this is what we managed to accomplish:
- We rewrote the project’s code and went from around 80% Objective-C to 95% Swift (mostly reactive)
- By doing that, we increased our productivity and decreased the time needed to implement new features by roughly 30% per feature (change request estimations)
- Since most iOS developers starting their career only learn Swift, we increased the pool of engineers who can jump on this project
- We removed more than two-thirds of the third-party libraries, which reduced the need for third-party library maintenance and improved our build time
- We opened the door for new technologies like SwiftUI, which is already taking hold in the project
At the end of the process, we sent the app to a pentest and got a positive grade. The very nature of Swift (and Apple’s code obfuscation) doesn’t allow a memory dump, which would allow someone to read out class and method names, which was an issue with Objective-C. That code dump can often be used for attacking the app with some sort of a code injection, so this is a large motivation for us to keep rewriting the code until everything is written in Swift.
It’s never too late to rewrite code
We all try to maintain our projects regularly, but as this example shows, without occasional rewrites or at least refactors, an application can fall back in stability and performance. Fixing bugs becomes slower, adding new features more challenging, and scaling the product nearly impossible. Not to mention the accrual of technical debt.
If you’re starting to detect code smells and your legacy code is giving you trouble, don’t give up. With a little planning and finding the best approach for your project, it’s not too late to make it shiny and new.
Maintain your code regularly, and as frequently as possible. You never know when the time will come to turn your ugly duckling into a beautiful swan.