Over time, every system accumulates design decisions that were fast or convenient at the time but that hinder future change. That gap between the ideal state of the code and its actual state is called technical debt, a financial metaphor coined by Ward Cunningham: just like monetary debt, it generates "interest" in the form of greater effort on every modification. Debt is not inherently bad —sometimes it is a sensible strategic decision— but ignoring it leads to paralysis. In this lesson we will study what it exactly is, how to classify it with Fowler's quadrant, how to measure it, and what strategies exist to pay it off, with continuous refactoring as the central practice.
Contents
- What technical debt is and the financial metaphor
- Fowler's technical debt quadrant
- Types and causes of technical debt
- How to measure technical debt
- Debt repayment strategies
- Continuous refactoring
- Common mistakes and tips
- Exercises
- Conclusion
- What technical debt is and the financial metaphor
Technical debt is the implicit future cost of rework caused by choosing a limited or quick solution now instead of a better approach that would take more time.
The financial metaphor has three key concepts:
- Principal: the effort needed to fix the problem (refactor, redesign).
- Interest: the extra cost you pay on every change while the debt exists (more slowness, more bugs).
- Leverage: sometimes taking on debt lets you deliver value sooner and "pay it back" later; it can be a rational decision.
graph LR
A[Quick decision] --> B[Technical debt]
B --> C[Interest: each change costs more]
C --> D{Is it paid?}
D -->|Yes| E[Principal settled: healthy code]
D -->|No| F[Compound interest: paralysis]Explanation of the diagram: a quick decision creates debt; while the principal is not settled, interest accumulates on each modification. If it is not managed, the interest compounds until the system becomes almost unmodifiable.
- Fowler's technical debt quadrant
Martin Fowler proposed a quadrant that classifies debt along two axes: whether it was incurred deliberately or inadvertently, and whether it was prudent or reckless. This nuances the moral judgment of debt.
| Reckless | Prudent | |
|---|---|---|
| Deliberate | "We don't have time for design" | "We ship now and accept the consequences" |
| Inadvertent | "What's a layer?" | "Now we know how we should have done it" |
Interpretation of each quadrant:
- Deliberate and reckless: you know how to do it right but decide not to out of laziness. The most dangerous.
- Deliberate and prudent: a conscious, justified decision (ship earlier to validate the market), with a repayment plan. Acceptable.
- Inadvertent and reckless: lack of knowledge or discipline; the team doesn't even know it is generating debt. Combated with training.
- Inadvertent and prudent: the debt of natural learning; only upon finishing do you understand the domain. Inevitable and healthy.
The lesson of the quadrant is that prudent and managed debt can be a strategic tool; reckless debt is a problem of discipline or knowledge.
- Types and causes of technical debt
| Type of debt | Example |
|---|---|
| Code | Duplicated code, huge methods, confusing names |
| Design/architecture | High coupling, absence of layers, circular dependencies |
| Testing | Insufficient coverage, fragile tests |
| Documentation | Outdated or nonexistent documentation |
| Infrastructure | Un-updated library versions, manual environments |
| Dependencies | Outdated libraries with vulnerabilities |
Common causes:
- Deadline pressure and rushed deliveries.
- Lack of knowledge or experience on the team.
- Requirement changes that invalidate previous decisions.
- Absence of time dedicated to maintenance.
- Staff turnover and loss of context.
- How to measure technical debt
Debt is partly intangible, but quantifiable indicators exist. Static analysis tools (such as SonarQube) compute metrics and a technical debt ratio.
Common metrics:
- Code smells and maintainability issues detected automatically.
- Cyclomatic complexity: the number of independent paths through a method; the higher it is, the harder it is to test and maintain.
- Code duplication (percentage).
- Test coverage.
- Technical Debt Ratio (TDR): estimated remediation cost divided by the development cost.
# Example of a quality gate configuration in SonarQube
sonar.qualitygate:
conditions:
- metric: new_coverage # coverage of new code
op: LESS_THAN
error: 80 # fails if <80%
- metric: new_duplicated_lines_density
op: GREATER_THAN
error: 3 # fails if >3% of duplicated lines
- metric: new_technical_debt # debt introduced by this change
op: GREATER_THAN
error: 60 # fails if it adds more than 60 min of debtExplanation: this quality gate prevents new code from degrading the project's health. Instead of requiring all historical debt to be cleaned up at once (unfeasible), it applies the "Clean as You Code" approach: new code must meet the standard, containing debt at its source.
Valuable indirect indicators:
- Decreasing team velocity with no apparent cause.
- Increasing defect rate.
- Rising lead time (time from commit to production).
- Debt repayment strategies
Not all debt should be paid off, nor in the same way. Main strategies:
| Strategy | What it involves | When to use it |
|---|---|---|
| Incremental repayment | Improve a little with each change (Boy Scout Rule) | By default, always |
| Dedicated refactoring | Reserve sprint capacity to settle debt | Localized, known debt |
| Partial rewrite | Rewrite a specific module | An unmanageable critical module |
| Full rewrite | Redo the system | Last resort, very risky |
| Tolerate the debt | Consciously do nothing | Stable code that isn't touched |
Prioritization: pay off first the debt that is in areas of high change and high impact. Debt in code that is never modified generates little interest; it is not worth settling. A useful matrix crosses "frequency of change" with "pain it causes."
Management recommendations:
- Make the debt visible (backlog, labels, a technical debt register).
- Allocate a fixed capacity (e.g., 15-20% of each iteration) to maintenance.
- Accompany every repayment with tests that protect the behavior.
- Continuous refactoring
Refactoring is improving the internal structure of the code without changing its external behavior. Continuous means doing it in small, constant steps, not in large, sporadic projects.
Golden rule: always refactor under the safety net of tests. Without tests, it isn't refactoring, it's risk.
// BEFORE: a long method, with several responsibilities and hard to follow
public double processOrder(Order p) {
double total = 0;
for (Line l : p.getLines()) {
total += l.getPrice() * l.getQuantity();
}
if (p.getCustomer().isVip()) {
total = total * 0.9;
}
if (total > 100) {
total = total; // free shipping
} else {
total = total + 5;
}
return total;
}// AFTER: extracting methods with expressive names
public double processOrder(Order p) {
double subtotal = calculateSubtotal(p);
double discounted = applyVipDiscount(p, subtotal);
return applyShipping(discounted);
}
private double calculateSubtotal(Order p) {
return p.getLines().stream()
.mapToDouble(l -> l.getPrice() * l.getQuantity())
.sum();
}
private double applyVipDiscount(Order p, double subtotal) {
return p.getCustomer().isVip() ? subtotal * 0.9 : subtotal;
}
private double applyShipping(double total) {
return total > 100 ? total : total + 5;
}Explanation of the refactoring: we apply Extract Method to separate each responsibility (subtotal, discount, shipping) into a method with a clear name. The external behavior does not change —which is why it is safe if there are tests— but the main method now reads like a description of the process. This reduces code debt and cyclomatic complexity.
Frequent refactorings: extract method, rename, extract class, replace conditionals with polymorphism, introduce parameter object. Modern IDEs automate them safely.
Common Mistakes and Tips
- Refactoring without tests. It is the most common cause of introducing bugs while "improving" the code. The safety net first.
- Mixing refactoring with functional changes in the same commit. Separate them: if something fails, you won't know whether it was the improvement or the new feature.
- Waiting for the "big refactor" or full rewrite. It almost always fails or never arrives. Debt is managed continuously and incrementally.
- Treating all debt the same. Prioritize by impact and frequency of change; ignoring this wastes effort on dead code.
- Not making the debt visible. What is not measured or recorded is not managed. Use the backlog and metrics.
- Confusing refactoring with rewriting. Refactoring preserves behavior in small steps; rewriting starts from scratch and is far riskier.
- Tip: adopt "Clean as You Code" to avoid degrading what is new, and the Boy Scout Rule to improve what you touch.
Exercises
Exercise 1. Classify the following situations in Fowler's quadrant: a) "We're launching the MVP with file-based persistence to validate the business, and we'll migrate to a DB if it works." b) "I copied and pasted the module because I didn't know interfaces existed."
Exercise 2. You have technical debt in two modules: one that is modified almost every week and another that hasn't been touched in two years. With limited resources, which do you prioritize and why?
Exercise 3. Refactor this method by extracting responsibilities (assuming tests exist):
public String generateGreeting(User u) {
String s = "";
if (u.getAge() < 18) s = "Hi there, young one "; else s = "Dear ";
s = s + u.getName();
if (u.isPremium()) s = s + " (premium customer)";
return s;
}Solutions
Solution 1. a) Deliberate and prudent: a conscious, justified decision, with a repayment plan (migrate if the business works). It is acceptable strategic debt. b) Inadvertent and reckless: generated through ignorance of the design. Combated with training and code review.
Solution 2. You prioritize the module modified every week. Debt generates "interest" on every change; in a high-change module that interest is paid constantly, so settling it has a high return. The module frozen two years ago generates little or no interest: tolerating its debt is the right choice as long as it doesn't need to be touched.
Solution 3. We extract each decision into a method with an expressive name:
public String generateGreeting(User u) {
return salutation(u) + u.getName() + premiumBadge(u);
}
private String salutation(User u) {
return u.getAge() < 18 ? "Hi there, young one " : "Dear ";
}
private String premiumBadge(User u) {
return u.isPremium() ? " (premium customer)" : "";
}The behavior is preserved, but the main method expresses the intent, and each rule is isolated and testable.
Conclusion
Technical debt is inevitable, but managing it is a decision. We have seen that the financial metaphor (principal and interest) explains why ignored debt becomes paralyzing, and that Fowler's quadrant helps us distinguish strategic debt from negligent debt. We learned to measure it with metrics and quality gates under the "Clean as You Code" approach, to prioritize its repayment according to impact and frequency of change, and to settle it through continuous refactoring under the protection of tests. With this lesson we close Module 2 on design principles and tactics: you now have a complete framework —from coupling and cohesion to debt management— for making conscious design decisions. The next module will make the leap to the architectural styles and patterns that structure complete systems.
Application Architecture Course
Module 1: Fundamentals of Application Architecture
- What Is Application Architecture?
- The Role of the Software Architect
- Quality Attributes and Non-Functional Requirements
- Architectural Decisions and Trade-offs
- Architecture Documentation: Views and the C4 Model
Module 2: Design Principles and Tactics
- Coupling, Cohesion and Separation of Concerns
- SOLID Principles Applied to Architecture
- DRY, KISS, YAGNI and Other Design Principles
- Architectural Tactics for Quality Attributes
- Managing Technical Debt
Module 3: Architectural Styles and Patterns
- Monolithic Architecture
- Layered Architecture (N-Tier)
- Client-Server Architecture
- Hexagonal Architecture (Ports and Adapters)
- Clean and Onion Architecture
Module 4: Distributed Architectures and Microservices
- Introduction to Distributed Systems
- Microservices Architecture
- Service Decomposition and Bounded Contexts
- API Gateway, Service Discovery and Inter-Service Communication
- Resilience Patterns: Circuit Breaker, Retry and Bulkhead
- The CAP Theorem and Data Consistency
Module 5: Event-Driven Architectures and Messaging
- Fundamentals of Event-Driven Architecture
- Asynchronous Messaging: Queues and Brokers
- Event Patterns: Event Sourcing and CQRS
- Managing Distributed Transactions: The Saga Pattern
- Real-Time Data Streaming
Module 6: Domain-Driven Design (DDD)
- Core DDD Concepts
- Strategic Design: Bounded Contexts and Ubiquitous Language
- Tactical Design: Entities, Aggregates and Repositories
- Context Mapping
Module 7: Data and Persistence
- Persistence Strategies: SQL vs NoSQL
- Data Access Patterns: Repository, Unit of Work and DAO
- Database per Service and Distributed Data Management
- Caching and Invalidation Strategies
Module 8: Cloud Architecture and Deployment
- Cloud Computing Fundamentals (IaaS, PaaS, SaaS)
- Containers and Orchestration with Docker and Kubernetes
- Serverless Architecture
- Cloud-Native Design Patterns
- Infrastructure as Code (IaC)
Module 9: Quality, Security and Observability
- Scalability: Horizontal vs Vertical and Load Balancing
- High Availability and Fault Tolerance
- Security by Design and Authentication/Authorization
- Observability: Logging, Metrics and Tracing
- Performance and Load Testing
