Starships on the internal combustion engine. Survive the battle with technical debt



How to survive the battle with technical debt? What to do if you have a legacy of a severe stage? In the article, using the example of three cases, I propose to figure out how to build the process of working with technical debt and which engineering approaches to use for this.

My name is Denis, I am the Backend Team Lead at Wrike. I am responsible for delivery in my team and for the growth of developers in several teams. It so happened that almost all of my experience is working at fintech. I have worked for two large banks, and now I work at Wrike.

At banks, you learn to work with systems where reliability and fault tolerance are important. And there’s a lot of Legacy there, and I experienced all this myself as a developer and helped others to experience myself as lead teams.

Wrike is a SaaS team collaboration solution that we sell to our customers. We make Wrike using Wrike to organize development.

Wrike develop thirty scrum teams. The service is available 24/7, so the decisions that we make must be reliable. If something goes wrong, then this will affect the work of almost all teams.

Why starships?


Starship is a metaphor that I use to describe what we programmers are working with. The application starts with several modules. Then it begins to grow: a big load comes upon it, microservices appear, communication between them, integrations, external APIs, clients. It turns out a large and connected ecosystem. The larger and older the application, the more connected the ecosystem becomes. And the more important it is to keep its significant nodes up-to-date and in a reliable state. It becomes very sad when the most important of them either do not meet today's requirements, or fail.
Your starship will not jump into the warp if it works on AI-95. The system will not navigate through four galaxies if the central computer is Intel Celeron.

What is technical debt?


Imagine that you have a feature in which bugs constantly appear. A tester comes to you and says: “This week we found four new bugs there.” Is it a technical debt or not? And if the presence of this feature blocks other stories that need to be done? And if it’s just hard to work with the solution, and you refactor it every time? If users complain about a feature? And if she does not meet the requirements that are imposed on her today, or offends the feelings of developers who strive for excellence?

In the article, by technical debt I will understand those features, solutions or approaches that interfere with further product development. All the problems described above are the result of technical debt. These are specific reasons: why he is and why you need to work with him.

Technical Debt Algorithm


To work effectively with technical debt, you need to ask three basic questions.

  1. What for? Why do we undertake to do something with him? Why do we need this work? Why spend company money, development hours, engineers time? There should be a clear understanding of what benefit we will get by solving a particular problem. From the definition it is clear that technical debt blocks the development of the product. Working with it, we will get new features and capabilities.
  2. What? We must clearly understand where the technical debt begins, where it ends, how it looks and how much work will have to be done to eliminate it.
  3. How? Actions that need to be done with technical debt in order to get rid of it.

To answer the last question, there is a simple iterative four-step algorithm. The first step is to highlight technical debt and understand its boundaries. The next step is to separate it from everything else: encapsulate, enter a contract of work between the solution that you have allocated, and the rest of the system. After that, you can create a new solution nearby, replace it, and, thus, part of the technical debt will leave the application. Repeating this iteration a number of times, you will get a ready-made solution.



Now let's see how the algorithm works in reality using several cases as an example.

First case


There is an application that works with orders from customers - order management system. The application was written long ago, in 2010, and is built on the latest technologies of that time. The application has been successfully operating in production for the past 9 years, but today the business understands that it is necessary to capture new markets and develop the system further. At the same time, it is important to save data and increase new functionality in the system.



It turns out that there are technologies that have long been dead, but there is also data that cannot be lost. Not all features can be implemented in an application using old technologies. Therefore, the situation, to be absolutely honest, looks something like this:



The problem here is not in the old frameworks, but in the situation that we have: the application is not supported, it is almost impossible to find developers for the frameworks of a decade ago. We have to do something about it.

Let's run the algorithm. Several parts of the technical debt can be distinguished and iteratively approach this process. First, let's deal with Frontend. We can launch the new Frontend using the old Backend. We will be able to expand the new Frontend, adapt it to modern technologies, it will meet our goals. We can either rely entirely on the old Backend, or it will have to be slightly modified to work with the new Frontend. The next step is encapsulation. With encapsulation, architecture helps us here. The encapsulation point in this case will be a contract with Backend. After we launched the new Frontend, we can remove the old part of Frontend. Now our entire application will become greener and greener.



The next step is working with Backend. Here, the encapsulation point will already be the database layer. It turns out that architecture will again do this encapsulation for us. And we can make a new solution nearby, working with the same data, and transfer Frontend to it. Now we have completely abandoned the old solution and can throw it away. This allows us to achieve the goal that we set for this project.



Second case


Take the case trickier. There is an application, it has a specific feature that is responsible for saving currency pairs in the database. For example, ruble-dollar, dollar-yen and so on. Information is stored in a database in a table. And to make it a little more fun, add a couple of dependencies: there is a consumer who receives data directly from the database, and a data provider who can supply it, again, directly to the database.



We are not happy with the data format and the way in which this data gets into the database. But you need to fix it carefully, there are many dependencies.

To do this, select a specific piece - data. You need to encapsulate them. To do this, enter the interlayer. Their task is to make sure that the consumer and the supplier do not notice any changes. This is the meaning of encapsulation. Now we can change the storage structure, because this will not affect external dependencies. After we can build a new solution that records data in a new format. And the last step is to transfer the old data to a new format and get what we wanted from our project: data in a new format, and the old logic can be removed from the application.


In this process, data consumers will not notice the changes, which means we did it completely safely, while maintaining backward compatibility. Then, if necessary for the project, you can work with the consumer and the data provider so that they also use the new format.

Third case


To increase the scale and understand how it works in large projects, imagine that there is a large project, a large code base and some kind of key functionality that sprouts to absolutely all points of the application. Other pieces of Backend use it, it has access to the Public API, that is, the data is leaking somewhere. The feature is used in Frontend, in external systems and even in the appendage goes directly from the database to analytics. To make the example more fun, add a pinch of Legacy here. Well, a bit.



Two fun legacy facts:

  1. It definitely works.
  2. No one knows how it works. That's why Legacy.

When working with such a case, in which there are many points of contact and a lot of unknowns, it’s worthwhile to understand: with what, indeed, we are working, what this solution looks like and what its capabilities are. It is important to understand how the solution we want to rework or want to get rid of interacts with the rest of the application. We need to find all common ground: to understand how they work, to understand their contracts in order to be able to offer something else.

There are several approaches here that can help, especially if the scale of the disaster is large enough in the amount of code:

  • . Java , , , . , , , , ;
  • . , , , . , , . , ;
  • -.

If we find common ground in the code, you can use them and wrap the solution with encapsulation points. We introduce new contracts for the interaction of the application with our feature.



Here the quantity of legacy was reduced, because at this very moment we are already starting to enter what we want into the application.

Next, you need to take the first step towards a new solution - tests. They will close the very funny fact about Legacy No. 2, which I mentioned earlier. Tests will show how your solution works. In addition to checking the key flow, you need to make sure that it also falls exactly where you expect it from him, and exactly the way you expect from him.

It often happens that a solution created once for business purposes was used 100%, and today it intersects with current goals only 30%, and 70% does not. In today's realities, these 70% are no longer important. The tests you wrote will highlight the very 30%. Having run the coated test, you can understand which code is not used at all, delete it and reduce the connectivity and complexity of your solution.

If we, having written the tests and understanding how it works, begin to introduce a new solution in the encapsulated area, we will gradually supplant everything that we don’t need, remove the legacy and replace the solution with a new one that suits our needs.



The new solution should be simple, understandable and should specifically solve your problem, specifically today and for specific immediate goals. There is no need to go deeper into overengineering, because we are closing today's ultimate goal.

And this is the place where you should stop, hang in the moment and think: “Why are we doing this?” At this stage, you will have a lot of information about how the solution works, what is in it and what is not. You wrote tests, marked up the code, made a diagram, and now you understand an order of magnitude more. Perhaps this is the point where it is worth stopping and understanding: is it possible to solve a much bigger problem? And this is what once saved the order of the development quarter for our project, because we chose the right direction for the development of the project.

How to organize work


We take an approach in which we try to break the new solution into iterations. These are its specific parts that can be rolled out in production and which will change something in it.

To understand what iteration is and what set of tasks it contains, we borrowed the concept of Definition of Done from Scrum. This is a set of criteria that must be met for a story to be considered fulfilled.

Here I use this concept in a slightly different form. By Definition of Done, I understand the description of what will change in the application when a particular iteration goes into production.

Let's remember the first example with the order management system. In it, the user could create a new order using the new UI and the old Backend. This is the type of iteration - a specific part of the functionality that we can roll out into production, and it will work. Or the user can log in with the new access rights model - this is also a qualitative change. So you can describe what exactly each iteration gives in your crusade in the fight against technical debt.

When you break the solution into iterations, there can be many problems. There will be a dependence between them, we get a whole graph. In this column there will be several ways to achieve the final result. You can use reverse planning - a tool that will help reduce work time. We start from the final point of the project and ask the question: “What needs to be done to achieve this goal?”. We understand that in order to achieve this goal, we must take the previous step. So, we move from the end to the beginning and at each intermediate step we answer this question, passing the critical path. Once such an approach saved us the development quarter.

A tool called the Gantt chart is well suited for visualizing work. This is a diagram that shows the relationships between tasks, their duration and how the project looks visually. In the picture is a screenshot from Wrike. We have this tool, we are actively using it to work with projects.



If you are working on an extensive solution, you may encounter a situation where someone changes the code that you refactor, adapt, guide in your process and that you just thought out. These changes can make it difficult for you to deal with technical debt.

You can protect yourself from such changes in several ways:

  • — . , , - , .
  • , . git hook, git commit git push. unit test, , - , , , : .

You can also configure an external monitoring system for your code. At Wrike we use PMD. The system starts up and checks for compliance with certain rules every new line of code. The picture is an example from the build log. Here the rule “public method results must be immutable” was violated, PMD talks about it and shows in which line - here it is the “wrong method” method. Below is a clue what you need to do with it in order to fix it. Thus, we always know where the rule is violated and how to fix it.



How to get through the final boss


An important thing that we have not talked about is the final boss, who has to go through while working with technical debt. Business, product owner, customer - everyone has a different name for it. It can interfere with dragging our engineering initiatives forward into production.

We all know about cases when the product owner did not push the most optimal solutions from an engineering point of view. But even if your product owner gazes at the titanium sheet, you can still negotiate with him. Working with technical debt, you open up new opportunities, and product owner needs them to develop their product.

You can agree on a time quota. For example, 10% of the time developers will devote to work on technical debt. Such a quota will not allow getting rid of technical debt, but will allow it not to swell.

Obviously, it is difficult to talk about technical debt, if it is not clear what exactly the conversation is about. Therefore, your team should have a technical backlog, in which there are tasks that are evaluated and prioritized by developers.

However, the above tools will not allow to deal with large-scale projects. And in this case, it is important, in terms of what has been listed earlier (3 questions to the technical debt, the project in the tracker, the critical path in the graph, etc.), to be able to explain the benefits of the project to your customer. And the more elaborate the story, the more understanding you will convey to it. A business has goals that you help it achieve. It is important for you to keep abreast of what is happening not only now, but also of plans that will become a reality in quarters. Therefore, communicate with the business, understand what it wants to do and what are the ways to achieve these goals, and use this knowledge to combine your engineering goals and business goals.

Perhaps the goals will not coincide immediately, but in a quarter or even in a year. But this will allow you to understand when the business will be ready for change.

If you manage to synchronize goals, you will receive all bonuses: prioritization, resources and organization of all work. Product owner will help you do this. The main task is to agree with him.

Total


When we talk about changes in key areas of the product, a simple four-step algorithm gets complicated:



Two more steps are added. The first is an understanding of what we are working with. The second - after encapsulation - an understanding of how the solution works and how to prevent changes in your code.

Using this algorithm and my recommendations for organizing the process, you can work with any technical debt.

The article is based on my speech at the meeting, you can see the video report.

All Articles