As developers, sooner or later we will end up in a situation where we have to work with a legacy codebase. That can be the whole application or just part of the application written at the very start of the project. It's hard to fight legacy as we don't know how each part of the application will grow and at which scale. Making everything easy to scale is not very efficient so you need to guess which parts of the application are most likely to be improved in the future.
But on one of our projects, we faced a unique challenge where we created a legacy even before the initial release. For quite some time we tried to fix it or improve it, but in the end, we decided that we should rewrite the whole project from scratch.
Here are the top 5 reasons why we ended up doing it and changes we made in a rewrite.
Before this one, we successfully developed more than 100 projects and we thought this should be just one more of them. The initial estimate was much bigger compared to anything else we were working on before. Just a few months into the project there were already red flags. The team was not comfortable with the chosen technologies as they had little experience with them. From the project very beginning, we used agile methodology in contrast to a waterfall on our previous projects. We started with very little documentation (as agile suggests) and worked from sprint to sprint. When the project grew in size, we faced constant refactors of our previous work to introduce new features. The combination of these few problems drastically increased feature estimates.
We also didn't have a lot of experience with the business domain, so we needed client input to discover even the basic information about their day-to-day work.
To fight these challenges, a few new roles were introduced: product manager, business analyst, and tech lead. Some were given to the experienced people already on the project, but some required completely new colleagues. Also, we decided to create a project specification with all the features, and a long-term development plan. This plan enabled us to develop features with upcoming features in mind. Together with all the organizational changes, we introduced new processes and improved old ones. This was supported by new tooling (Jira, Confluence) and integrations with tools already in our stack (GitHub, CI:CD).
One of the main reasons why we faced some walls during development is technology choice for a project of such size. Our core technologies were not a problem, but JSON:API as API standard gave us plenty of issues. We had a lot of actions on top of domain models and this didn't work very well with the resource-based approach. In addition to this, maintaining API documentation took us too much time. Generators helped with new resources, but not so much with modifying old ones.
In a rewrite, we swapped JSON:API with GraphQL which gave us much-needed flexibility in communication between API and front-facing app. Accessing the data remained very similar to what we used to in JSON:API, but mutations allowed us to modify data in a more action-focused way.
We were very comfortable with MVC, so we decided to keep it in this project as well. The only difference is that we switched to a headless architecture where a back-end team develops API and a front-end team develops a front-facing app. When the project scaled, folder structure became very messy and there were no restriction to use a logic of multiple domain contexts in the same place. With everything mentioned, adding new features to such codebase became very hard even with a good code coverage in tests.
This was one of the hardest challenges to fix as we had to change it from the ground up. After some research and a few prototypes, we decided to use DDD and Layered architecture principles in our codebase. It gave us a very descriptive folder structure in every bounded context and boundaries for cross-context communication.
In a project with a complex domain and messy codebase with no clear structure, onboarding of new colleagues was very difficult. It took us around 3 months before a new person on the project became productive. As this was a long-term project, it's expected that from time to time we will need to onboard a new person, and losing too much time on it is something we wanted to avoid.
Just by implementing a new architecture, everything mentioned here was drastically improved. We had an option to onboard people only in one specific domain context and they could finish their tasks without worrying about breaking other parts of the application.
Every developer wants to work on an interesting, challenging, well-structured project with little to no stress. With everything we were facing, it was very hard to keep a good team spirit. Instead of feeling proud on finding the solution for a complex domain problem, we faced technical challenges where domain logic was very simple. This made us feel like we are not progressing as we should.
Clear hierarchy, roles, and responsibilities in combination with modern technologies drastically improved team morale. To ensure it stays this way, after every sprint, we grade our happiness and try to figure out what could be improved.
It's never an easy decision to abandon everything you were building for years, but sometimes this is the best possible decision. It took us 10 months to rewrite everything and during this process, we didn't face any major blockers or technical limitations. The team became more stable and fueled by the same goal. By looking at the project's future, the rewrite was the best decision we could make. The mistake was that we didn't do it earlier.