We're ready to start our journey. We have an application that's riddled with technical debt, woefully out of date, or just generally underserving its users, and so we need to understand what our best option is going forward - does it make more sense to continue to plod along and incrementally refactor? Or do we blow it all up and rewrite from scratch? This is the basic dilemma we'll be exploring in this book. So let's get going...
But not so fast! Before we go any further we need to address the elephant in the room, which is this: for any legacy application in need of improvement, what to do next is not a simple this-or-that decision. We may frame our options generally as rewrite or refactor, but these terms, as we'll see, are really just stand-ins for a whole spectrum of choices that lie before us.
By refactoring, within the context of modernizing a legacy application, we typically mean that we're going to keep the application mostly as it is, but make some minor, internal improvements to solve specific problems (like maintainability, extensibility, etc.). By rewriting, on the other hand, we imply that we intend to "start again from scratch", or in other words to make major changes.
But this only begs the next question! What exactly do we mean by minor and major? If we plan to upgrade our frontend framework from AngularJS to React but keep the backend services as they are, is that a refactor or rewrite? Or what about if we want to break a monolith into three different microservices, but the business logic is just copied-and-pasted into new version-control repos - is that a rewrite, refactor, or something else? And do we even care? Does labeling our effort really matter?
Yes, it does. Although our job is to build working software and not philosophize about semantics, the words we use do make a difference. When we suggest the path of rewriting or to refactoring, business and technology stakeholders should understand exactly what we mean and what type of effort will be entailed. In other words, some precision in our terms will help us better set expectations. Further, as we burn through some of this conceptual fog and find clearer definitions, it will also give us a more nuanced view of this decision, and move us beyond the narrow rewrite or refactor framing.
So like any big journey, let's spend a little time packing before we jump in the car and go. We don't want to show up at the beach and realize we forgot our swim suit.
A good place to start is to define what rewrites and refactors are not, which are strategies to improve the functionality of an application. This type of work, whether it's fixing defects, delivering new features, or cleaning up the user interface, we can call enhancement. It's about improving on what the application does for its users, and as we'll see later, it's the normal state of development.
In some cases, the scope of the functional enhancements can be quite large though. The business may determine that the app serves the right user base, for example, but the features all need to be overhauled. It may be tempting to call this case a rewrite as well, however we're going to make a distinction here. Because this type of effort entails building all new functionality, it is basically indistinguishable from greenfield development. When new functional requirements need to be defined, a separate system is developed from scratch, and no carry over of logic or code is possible, we will consider this to be development of a greenfield app and not a rewrite.
What we mean by Refactoring
Adding functionality to an application is not what this book is about. Our situation is that the application generally does what it is expected to do, but is lacking in the how - in other words, the non-functional or quality attributes of the system. For example, users might be happy with the set of features, but the application might be excessively difficult to maintain, or it may crash frequently, or may perform poorly under peak load. It's when these non-functional attributes are lacking that we consider a rewrite or refactor.
With respect to refactoring, we often use this term to refer to different scopes of work. In his book Refactoring, Martin Fowler defines it this way:
Refactoring is a disciplined technique for restructuring an existing body of code, altering its internal structure without changing its external behavior.
In this purist sense, refactoring is primarily about making the code more maintainable. This might be decomposing long or complex functions, fixing inconsistent naming, adding unit tests, or restructuring class hierarchies, data structures, or schemas. Note that nothing that is visible to the user changes, but the internal code structure is modified to make it easier to work with for developers, thus improving our productivity (and happiness!).
In the context of our rewrite or refactor decision, however, this definition is too restrictive. When we talk about refactoring in this context, we're not typically making a distinction between internal and external, but rather about functional and non-functional. For example, we may say that we are choosing to refactor the existing code base to improve the reliability or performance of the application. These quality attributes are technically not internal properties of the system (they are very visible to the users in that they directly impact them), they are just non-functional. This might be a pedantic distinction, but in the spirit of precision, I think it's important to call out. In this book we'll be using a broader definition of refactoring:
Refactoring is an approach by which an existing body of code is incrementally restructured in order to improve the quality attributes of the system.
Lastly, it's important to note that refactoring is about iterative change. It's making minor tweaks to the application, delivering them, and then rinse and repeat. Layered in with functional enhancements, refactoring can keep our users happy and our codes base healthy, minimizing technical debt and feature gaps. When neglected, however, we may need to consider the more heavy-weight alternative.
What we mean by Rewriting
Like refactoring, rewriting has the same basic goal - to improve the non-functional nature of the application. The difference is in how much is changed. Simply put, if refactoring is duct tape, rewriting is a sledge hammer or a back hoe. It's not about making incremental improvements to what is there, it's about blowing it up and building anew. With respect to the other types of development efforts we've discussed, we can visualize a rewrite this way:
We can say then that a rewrite is an effort that involves major changes to the system in order to make fundamental improvements to its quality attributes. But there is gray area. Rewrite efforts often spill over into the other quadrants. For example, an application may be so crippled by technical debt that it's virtually impossible to add new features. We may choose then to rewrite and build a new foundation to improve maintainability and extensibility (quality attributes), but during the course of that rewrite we may also sneak in a few new features to satisfy the business. It's fundamentally a rewrite, but there is some enhancement going on as well.
Likewise, there is some fuzziness at the boundary of rewrite and refactor. There are lift-and-shift scenarios where the system is moved to a new platform making it essentially a different application, but the code implementation within it is mostly the same - i.e. not refactored. This feels like a rewrite, but is it? How much do we have to change to consider it a rewrite?
Again, let's see if we can add some precision. For the purpose of this book, let's use the following definition:
Rewriting is re-building the same functionality that exists in a legacy application but using a different language/framework, maintaining within a new code repository (not just a branch), and deploying as an entirely new artifact (possibly to a different platform - e.g. servers, hardware, serverless, client, etc.).
In other words, we're drawing some clear lines. If, for example, we're rewriting an important function, class, or even module, but our work is made in a branch off of the mainline of our codebase, it's not a rewrite. Likewise, if we re-implement a segment of the application, but the system itself is still deployed as the same artifact (binary, WAR, etc.), this also is not a rewrite. Rewrites within our context are BIG. They are about major changes that necessitate an entirely new application to be built and deployed. Yes, there may be incremental steps along the road to get there, as we'll see later, but it is a fundamentally different effort than refactoring.
To help clarify things in your individual case, it can be helpful to diagram this out. For the different possible paths for modernizing or improving your application, what exactly is being changed? Here's an example:
In practice, the nature of the changes may not line up neatly with the definitions of rewrite or refactor, and that's ok. The diagram above, for example, might represent a case where we're proposing to re-implement a service using a more modern set of technologies, but while keeping the exposed API and underlying persistence structure the same. This represents a blend of minor and major changes, so it still may not be clear precisely what to label it. What's important, however, is that we've arrived at deeper level of detail which will help us better reason about and justify the decision.
Now we're almost done with our preparations, but before we get on with our journey, let's put it all together and see how these different types of development efforts all fit within the lifecycle of a given application. In other words, let's explore the origin story for a rewrite.
The Rewrite Origin Story
It all starts in the Greenfield Development stage, where we take an idea and begin to build from it a functioning application. After weeks or months of dogged effort, something is eventually Launched to "market" (which may be actual paying customers, or just a set of internal business users, etc.). If the app is well received, it lives in the equilibrium stage of Enhancement for some period of time, where new features are added and defects are resolved. Everyone is happy. Eventually though, technical debt can accumulate, and we begin to see Diminishing Returns in our effort - whereas a week of development used to be enough to add an entire new feature, it might barely suffice now to change the color of a button.
It's at this point that we might question whether it's worth investing more time and money. Further, since the app was first launched, some exciting new technologies have probably emerged, and we may have some grand visions for how they could be leveraged to make our app more resilient, easier to work with, performant, etc. And so we begin to make a plan for a rewrite. The idea is to freeze development on the existing system for some short period of time, and then shift resources over to work on the replacement system. We'll first build the foundation (using more modern patterns, tools, languages, etc.) and then next migrate the existing functionality into it. Users will just need to ride out the "pause" (i.e. not get any new updates), but when the rewritten system is in place, productivity should be twofold (or more!) what it was before.
And while this plan seem may seem straight forward, it belies some critical risks - technical, organizational, and psychological factors that all conspire to make this rewrite stage extremely precarious. And as this phase drags out, our chances making it out with a successful replacement get dimmer and dimmer. In the next two chapters we'll explore some of these dangers lurking within a rewrite effort, and then the reasons why we often forge ahead anyway in spite of them.