The client asks: "If adding this feature in a new application would take a week, why does it take three weeks here?"
That is a very good question.
And the answer often is not: "because the developers work more slowly."
The problem may lie much deeper - in the system architecture, its dependencies, the way data is stored, lack of tests, historical decisions, and successive changes added over the years.
That is precisely why the cost of software development is not fixed.
The same feature can cost a completely different amount in two different systems.
Code is not priced only by the number of features
At first glance, a task may seem trivial.
"Let's add the ability to export data to Excel."
Or: "Let's add a new user role."
Or: "Let's connect the system to our CRM."
The problem is that a feature never exists in complete isolation from the rest of the system.
A new feature may require changes in:
- the database,
- the API,
- the backend,
- the frontend,
- the permissions system,
- logging,
- reporting,
- integrations,
- tests,
- cache mechanisms,
- documentation,
- the deployment process.
The more interconnected the system is, the more elements need to be analyzed before making a change.
The biggest cost may arise before the first line of code is written
In a mature system, a developer should not simply start writing.
First, you need to answer:
- Where should this feature be added?
- Which modules will it communicate with?
- What data does it use?
- Do the existing permission mechanisms cover it?
- Will the change affect other processes?
- Which tests need to be updated?
- Does the current architecture even allow this to be done properly?
All of this is part of the cost of delivering the feature.
That is why, in an old system, a significant part of the work may not be programming itself, but identifying the dependencies and limitations of the existing solution.
Technical debt works like interest
A good way to think about technical debt is precisely the cost of subsequent changes.
If a certain solution was once implemented quickly, that may have been entirely justified.
The problem arises when a temporary solution becomes a permanent part of the system.
Another feature appears.
Then another.
An exception is introduced.
Then another exception.
Then come an integration, a workaround, a manual process, and an additional rule.
After a few years, no one remembers why the system works the way it does.
But each subsequent change must take all of those historical decisions into account.
Martin Fowler describes technical debt as the extra effort incurred when changing a system due to problems with its internal quality.
So we can say: technical debt does not have to stop development immediately. At first, it simply makes every subsequent change more expensive.
Signal one: "while we're at it, we also need to fix five more things"
This is one of the most characteristic symptoms.
The client orders one feature.
During analysis, it turns out that to implement it, you need to:
- fix the table structure,
- change the authorization method,
- update the library,
- fix the old API,
- rewrite part of the frontend.
Suddenly, a small feature stops being a small feature. Not because the requirement is complicated. Because the system no longer has the right architectural boundaries.
Signal two: one change requires testing the entire system
If a minor modification requires a full manual regression test, the organization is paying for a lack of automation.
As the system grows, the number of possible combinations increases.
Without an adequate set of tests, it becomes increasingly difficult to be sure that the new feature did not break the old one.
That, in turn, leads to caution.
Deployments become rarer.
Changes become bigger.
Risk increases.
And larger deployments are harder to diagnose when problems occur.
A vicious cycle emerges.
Signal three: "better not touch that module"
That sentence should raise a warning flag.
If a specific module has become an area the team avoids because its behavior is unpredictable, the system has a serious maintainability problem.
It is even worse if only one person knows how it works. Then the company has not only technical debt. It also has knowledge risk.
When one employee leaves, the company may lose the knowledge needed to safely develop the system.
Signal four: every feature requires exceptions
A well-designed system should have predictable rules.
If every new feature requires adding a special exception, an additional condition, or an individual path, the architecture is probably starting to constrain growth.
This often leads to code that can no longer be easily predicted.
And a lack of predictability means higher costs for analysis, testing, and maintenance.
Do you need to rewrite everything?
No.
And this brings us to a very important distinction.
Technical debt does not automatically mean a rewrite is necessary.
Possible solutions include:
Refactoring
That is, improving the structure of the existing code without changing its business behavior.
This is a good direction when the system still has a sensible architecture, but specific parts are hard to maintain.
Modernizing selected components
There is no need to replace the entire application.
You can start with the most problematic module, integration, or layer.
Gradual migration
New elements can run alongside the old system, and subsequent areas are moved over successively.
This approach helps limit the risk of a one-time migration. In the literature on legacy modernization, gradual extraction of functionality and replacement of successive parts of the system is often used.
Rewrite
Building a new system makes sense when the current architecture is so restrictive that further modernization no longer provides a justified return.
But a rewrite should be a decision based on analysis, not a reaction to team frustration.
When is it not yet worth investing in modernization?
Technical debt in itself is not a reason to stop development. Every system has some level of technical debt. Sometimes paying it down does not make economic sense.
If the application:
- is stable,
- is secure,
- has a small number of changes,
- supports a process that will not grow significantly,
- does not generate operational problems,
it may be reasonable to leave it as it is.
It is not about every system being technologically perfect.
The point is for the level of debt to be a conscious decision.
When does the cost of debt become a business problem?
When it starts affecting the company’s results.
For example:
A new feature was supposed to hit the market in a month, but it needs three.
Integration with a new partner drags on because the old system’s API does not easily support new data.
A key person on the team has to be involved every time, because only they know the old module.
Every major deployment requires hours of regression testing.
A competitor introduces new features faster because their platform makes experimentation quicker.
At that point, technical debt stops being an IT department problem.
It becomes a business problem.
How can you measure whether the situation is getting worse?
You do not need to build a complicated KPI system.
It is worth monitoring a few simple indicators:
Lead time - how much time passes from starting work on a change to deploying it.
Deployment frequency - how often the team can safely deliver changes.
Change failure rate - how often deployments cause problems.
Recovery time - how quickly you can return to stable operation after an outage.
Feature delivery time - whether similar tasks require more and more effort.
It is also worth analyzing the number of manual operations, test coverage, dependency freshness, and the time needed to onboard a new developer to the project.
Such data makes it possible to see whether the problem is truly technical or perhaps stems from the process, requirements, or the way work is organized.
The worst solution is "one more quick fix"
If the team knows the architecture needs changes but keeps postponing the topic every time, the system can enter a spiral.
"Let’s do a workaround for now."
"We’ll do the refactoring later."
"For now, this is enough."
"At the next release."
The problem is that the next release brings more requirements.
And every additional workaround increases the cost of the next change.
That is why the decision to pay down technical debt should be part of the product development strategy, not a random reaction to a crisis.
A good application is not one that never gets old
Every system will change.
Technologies will change.
Customers will have new needs.
New integrations will appear.
The way the company works will change.
That is why the goal should not be to create an application that never needs modernization. The goal should be to create an architecture in which modernization is possible without stopping the business. That is a huge difference.
Because the best system is not the one that looks the most modern on launch day. It is the one that, even after a few years, still allows the company to respond quickly to change.
And if every new feature costs more and more, that does not always mean the feature is difficult.
Perhaps the system itself has already become difficult.
