Technical Debt

 

What Is Technical Debt?

Technical debt is the future cost you incur when you choose a faster or easier solution now instead of a better long-term one.

The term was popularized by Ward Cunningham.

Think of it like financial debt:

  • You borrow time now (ship faster)

  • You pay interest later (harder changes, more bugs, slower velocity)

Not all technical debt is bad. Sometimes it’s a strategic decision.


Types of Technical Debt

Intentional (Strategic)

You knowingly cut corners to:

  • Hit a deadline

  • Validate a feature

  • Test market demand

This is acceptable — if tracked and repaid.


Unintentional (Accidental)

  • Poor design decisions

  • Lack of experience

  • No architecture thinking

  • Rushed hacks that became permanent

This is dangerous because it grows silently.


Bit Rot / Entropy

  • Dependencies outdated

  • Tests flaky

  • Architecture no longer fits new features

  • Code becomes harder to understand over time

Even good systems accumulate this.


How Technical Debt Hurts a Backend Java System

In backend Java (especially Spring apps), debt often shows as:

  • Huge service classes

  • Tight coupling between layers

  • Massive @Transactional methods

  • Anemic domain models

  • Duplicate validation logic

  • Hard-to-test business logic

  • Slow build times

  • Fear of changing core modules

Symptoms:

  • Each feature takes longer than the previous one

  • Bug rate increases

  • Onboarding new devs is slow

  • Refactoring feels risky

That’s interest compounding.


How to Manage Technical Debt

Make It Visible

Untracked debt becomes permanent.

Track it:

  • Jira tickets labeled “Tech Debt”

  • Architecture backlog

  • Code comments with ticket references (not TODO graveyards)

If it’s not visible, it won’t get prioritized.


Distinguish Good vs Bad Debt

Ask:

  • Did we choose this consciously?

  • Does it still serve a purpose?

  • Is the interest acceptable?

Sometimes debt is fine if:

  • The feature might be removed

  • The system is temporary

  • The cost of fixing exceeds benefit


Pay It Gradually (Best Strategy)

Don’t schedule “Refactor Everything” sprints unless architecture is collapsing.

Instead follow the Boy Scout Rule (from Clean Code):

Leave the code cleaner than you found it.

While adding features:

  • Extract methods

  • Rename variables

  • Reduce duplication

  • Add missing tests

Small continuous improvements beat massive rewrites.


Refactor When Touching the Code

Best moment to reduce debt:

  • When modifying the same module

  • When adding related features

  • When fixing a recurring bug

You already understand that area — cheapest time to improve it.


Protect With Tests First

Never refactor risky areas without:

  • Unit tests

  • Integration tests

Tests reduce refactoring fear.

No tests → write minimal safety tests first.


Measure the Impact

You don’t manage what you don’t measure.

Signals of growing debt:

  • Deployment frequency decreasing

  • Lead time increasing

  • Rising bug count

  • Growing PR size

  • Developers avoiding certain modules

If velocity drops over time, debt may be the cause.


Avoid the Rewrite Trap

Full rewrites are often:

  • Emotionally satisfying

  • Technically risky

  • Business dangerous

Most large rewrites fail because:

  • Requirements change

  • Hidden complexity emerges

  • New system accumulates new debt

Prefer incremental refactoring.


Practical Backend Java Example

Bad debt pattern:

@Transactional
public void processOrder(OrderRequest request) {
   validate(request);
   User user = userRepository.findById(...);
   inventoryService.checkStock(...);
   paymentService.charge(...);
   emailService.send(...);
}

Over time:

  • More conditionals added

  • More dependencies injected

  • Harder to test

  • Bugs appear in edge cases

Managing debt here might mean:

  • Extracting orchestration layer

  • Moving business rules into domain model

  • Introducing events

  • Reducing transaction scope

  • Writing focused unit tests


When to Pay Debt

Pay it when:

  • It slows feature delivery

  • It causes recurring bugs

  • It blocks architectural evolution

  • It increases onboarding time

  • The interest > business value gained

Don’t pay it when:

  • Code is stable and rarely touched

  • System will be deprecated soon

  • Business priorities are elsewhere


Senior Engineer Mindset

Technical debt isn’t a failure.

It’s:

  • A trade-off decision

  • A business conversation

  • A risk management problem

The real problem is unmanaged debt.


When to Refactor a Code

Refactoring isn’t about rewriting code because it feels “ugly.” It’s about improving design without changing behavior—at the right time.

Here’s when you decide to refactor a code.


1. When You See “Code Smells”

Refactor when you notice structural problems that make the code harder to maintain:

  • Duplication – Same logic repeated in multiple places

  • Long methods – Functions doing too many things

  • Large classes – One class handling multiple responsibilities

  • Deep nesting / complex conditionals

  • Poor naming – Variables like datatempx

  • Shotgun surgery – Small feature change requires editing many files

  • Feature envy – A method using another class’s data more than its own

If the code makes you hesitate before modifying it, that’s a signal.


2. The Best Time: While Adding or Changing Features

The ideal moment to refactor is:

When you're already touching the code.

This follows the Boy Scout Rule (popularized in Clean Code by Robert C. Martin):

“Leave the code cleaner than you found it.”

Why?

  • You already understand that part of the system.

  • You reduce future friction.

  • You avoid large risky rewrites.


3. When Change Is Becoming Expensive

Ask yourself:

  • Does every small change feel risky?

  • Are bugs appearing in unexpected places?

  • Are tests hard to write?

  • Does onboarding new developers take too long?

If yes → the design is slowing you down → refactor.


4. When You Have Tests

Refactor only when behavior is protected by tests.

If there are no tests:

  1. Write tests first.

  2. Then refactor.

Without tests, refactoring becomes guessing.


5. When Complexity Is Growing Faster Than Value

Sometimes code “works fine” but is getting harder to understand.

A useful heuristic:

  • If it takes longer to understand the code than to write it → refactor.

  • If adding a simple feature requires major mental overhead → refactor.


When NOT to Refactor

  • Right before a critical release

  • Without understanding the system

  • Just for aesthetic reasons

  • In stable legacy code that rarely changes

  • When there’s no test safety net

Refactoring is a tool—not a hobby.


A Practical Rule of Thumb

Refactor when at least one of these is true:

  1. You are modifying the code anyway.

  2. The code is actively slowing development.

  3. The design blocks new features.

  4. Bugs keep recurring in the same area.


Strategic vs Opportunistic Refactoring

Opportunistic – Small improvements during feature work
Strategic – Planned refactoring sprint to fix structural issues

Use strategic refactoring when:

  • Architecture is the bottleneck

  • Technical debt is measurable and costly

  • The team agrees it's needed


Simple Decision Framework

Ask:

  1. Does it work?

  2. Is it hard to change?

  3. Will it need to change again?

If:

  • Works + won’t change → leave it.

  • Works + will change → refactor.

  • Doesn’t work → fix first, then refactor.


Here’s when you should refactor in a Java backend, and what that usually looks like in practice.


1. Your Service Class Is Doing Too Much

Smell:

A single UserService:

  • Validates input

  • Talks to repository

  • Maps DTOs

  • Handles transactions

  • Sends emails

  • Logs metrics

That violates Single Responsibility Principle (from Clean Code by Robert C. Martin*).

Refactor when:

  • Methods exceed ~30–40 lines

  • Constructor has 6+ dependencies

  • Unit tests are painful to write

Fix:

Split into:

  • UserValidator

  • UserMapper

  • UserRepository

  • EmailService

  • Keep UserService as orchestrator


2. Duplicate Logic Across Controllers or Services

Smell:

Same validation or mapping repeated in:

  • UserController

  • AdminUserController

  • BatchUserProcessor

Refactor when:

You copy-paste code even once.

Fix:

Extract shared component:

@Component
public class UserValidator { ... }

Duplication spreads bugs.


3. Complex Conditionals (Especially in Business Logic)

Smell:

if (type.equals("A")) {
   ...
} else if (type.equals("B")) {
   ...
} else if (type.equals("C")) {
   ...
}

Refactor when:

Adding a new type requires editing this method.

Fix:

Use polymorphism or strategy pattern.

Example:

interface PricingStrategy {
    BigDecimal calculate(Order order);
}

Add new implementations instead of editing existing logic.


4. Anemic Domain Model (Very Common in Spring Apps)

If your entities are just:

class Order {
   private BigDecimal amount;
   private String status;
}

And all logic is in services:

→ That’s a sign to refactor.

Move behavior into the domain:

public void cancel() {
   if (status.equals("SHIPPED")) {
       throw new IllegalStateException();
   }
   this.status = "CANCELLED";
}

Refactor when business rules are scattered everywhere.


5. Tests Are Hard to Write

If unit tests require:

  • 5 mocks

  • Complex setup

  • Spring context loading

That’s a design smell.

Refactor toward:

  • Constructor injection

  • Small classes

  • Pure functions when possible

If you need @SpringBootTest for simple logic → refactor.


6. Transaction Boundaries Are Confusing

Common backend issue:

@Transactional
public void processOrder() {
   updateInventory();
   chargeCard();
   sendEmail();
}

If failures cause inconsistent states:

→ Time to refactor.

Split orchestration and side effects.
Consider domain events.


7. Repository Layer Is Leaking Into Business Logic

If you see this in many services:

userRepository.findById(id).orElseThrow(...)

Repeated everywhere?

Refactor into:

public User getRequiredUser(Long id)

Encapsulate repository behavior.


8. Adding Features Feels Risky

Ask yourself:

  • Does adding one endpoint require touching 5 files?

  • Does changing DB schema break many services?

  • Are regressions common?

If yes → refactor before adding more features.


When NOT to Refactor for Java Backend

Don’t refactor when:

  • System is stable and rarely changed

  • You’re days before production release

  • No tests exist

  • It’s legacy code no one touches


Practical Rule for Backend Java

Refactor when:

  • Class > 300 lines

  • Method > 40 lines

  • Constructor > 5 dependencies

  • Cyclomatic complexity feels hard to reason about

  • You fear touching the code


Senior Developer Heuristic

If reading a method requires scrolling → refactor.
If understanding a change requires jumping across many classes → refactor.
If business rules are not obvious → refactor.



Abstraction vs Encapsulation

 In Java, abstraction and encapsulation are both core Object-Oriented Programming (OOP) concepts, but they serve different purposes.



Abstraction

What it means:

Abstraction is hiding implementation details and showing only essential features.

It focuses on what an object does, not how it does it.


How it’s achieved in Java:

  • abstract classes

  • interfaces


Java Example (Abstraction using interface)

interface Payment {
    void pay(double amount);  // only method declaration (what to do)
}

class CreditCardPayment implements Payment {
    @Override
    public void pay(double amount) {
        System.out.println("Processing credit card payment of $" + amount);
    }
}

public class Main {
    public static void main(String[] args) {
        Payment payment = new CreditCardPayment();
        payment.pay(100);
    }
}


Explanation:

  • The Payment interface defines what should be done (pay()).

  • The implementation details (how payment is processed) are hidden inside CreditCardPayment.

  • The user of Payment doesn’t need to know how it works internally.



Encapsulation

What it means:

Encapsulation is wrapping data (variables) and methods together and restricting direct access to some of the object’s components.

It focuses on data protection.


How it’s achieved in Java:

  • private variables

  • Public getters and setters


Java Example (Encapsulation)

class BankAccount {
    private double balance;  // hidden data

    public double getBalance() {
        return balance;
    }

    public void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
        }
    }
}

public class Main {
    public static void main(String[] args) {
        BankAccount account = new BankAccount();
        account.deposit(500);
        System.out.println(account.getBalance());
    }
}


Explanation:

  • balance is private → cannot be accessed directly.

  • Access is controlled through deposit() and getBalance().

  • This protects the object’s internal state.



 Key Differences

AbstractionEncapsulation
Hides implementation detailsHides internal data
Focuses on behavior (what)Focuses on data protection
Achieved using interfaces & abstract classesAchieved using access modifiers
Design-level conceptImplementation-level concept


Simple Analogy

  • Abstraction → Driving a car: You use the steering wheel and pedals without knowing how the engine works.

  • Encapsulation → The car’s engine is hidden under the hood so you can’t directly modify its parts.


SOLID principles

SOLID is a bundle of five object-oriented design principles that help you write code that’s easier to understand, change, and not hate six months later
Here’s the rundown, with a practical lens.


S — Single Responsibility Principle (SRP)

A class should have one reason to change.

In practice:
If a class does too many things, changes ripple everywhere.

Bad smell

UserService:
- validates users
- saves users to DB
- sends welcome emails

Better

  • UserValidator

  • UserRepository

  • EmailService

💡 Most-used in real life. Even outside OOP, this principle saves you constantly.


O — Open/Closed Principle (OCP)

Open for extension, closed for modification.

In practice:
You should be able to add new behavior without editing existing, tested code.

Example

  • Use interfaces / abstract classes

  • Use strategy patterns instead of if/else explosions

PaymentProcessor
  ├── CreditCardPayment
  ├── PaypalPayment
  └── CryptoPayment

Add a new payment type → no need to touch old code.

💡 Used a lot in frameworks, plugins, and business rules.


L — Liskov Substitution Principle (LSP)

Subtypes must be usable anywhere their base type is expected.

In practice:
If subclassing breaks expectations, you’re in trouble.

Classic red flag:

Square extends Rectangle

Because changing width breaks height assumptions.

💡 You mostly notice LSP when it’s violated—bugs appear in “perfectly valid” polymorphism.


I — Interface Segregation Principle (ISP)

Clients shouldn’t depend on methods they don’t use.

In practice:
Prefer small, focused interfaces over giant god-interfaces.

Bad

Machine:
- print()
- scan()
- fax()

Better

  • Printable

  • Scannable

  • Faxable

💡 Super common in APIs and microservices—especially when contracts evolve.


D — Dependency Inversion Principle (DIP)

Depend on abstractions, not concrete implementations.

In practice:
High-level code shouldn’t care how things are done.

OrderService → PaymentGateway (interface)
                   ↑
        StripeGateway / PaypalGateway

This enables:

  • Testing with mocks

  • Swapping implementations

  • Cleaner architecture

💡 Heavily used with dependency injection frameworks.


Which SOLID principles get used the most in practice?

If we’re being honest:

  1. Single Responsibility (SRP) — constantly, everywhere

  2. Dependency Inversion (DIP) — especially in testable systems

  3. Interface Segregation (ISP) — once systems grow

  4. Open/Closed (OCP) — when extensibility matters

  5. Liskov (LSP) — more of a guardrail than a daily tool

Or said differently:

  • SRP & DIP → daily habits

  • OCP & ISP → design decisions

  • LSP → “don’t mess this up”


Internet of Things (IoT) and Embedded Systems

The Internet of Things (IoT) and Embedded Systems are interconnected technologies that play a pivotal role in modern digital innovation. Here’s a detailed overview of their relationship, applications, and significance:


1. Internet of Things (IoT):

Definition:
IoT refers to a network of physical devices ("things") embedded with sensors, software, and connectivity to exchange data over the internet.

Key Features:

  • Connectivity: Devices are interconnected through wireless or wired networks.
  • Data Collection and Sharing: Devices gather real-time data and share it across networks for processing and decision-making.
  • Automation and Intelligence: Leverages AI/ML for smarter and adaptive systems.
  • Remote Accessibility: Enables remote monitoring and control of devices.

Components:

  1. Devices and Sensors: For data collection (e.g., temperature, motion, light sensors).
  2. Network: Communication protocols (e.g., Wi-Fi, Bluetooth, Zigbee, LoRaWAN).
  3. Cloud and Edge Computing: Data processing and analytics.
  4. Applications and Interfaces: User access via apps or dashboards.

Applications:

  • Smart Homes: Connected thermostats, lighting, and security systems.
  • Healthcare: Remote patient monitoring and fitness trackers.
  • Industry (IIoT): Predictive maintenance, smart factories.
  • Agriculture: Precision farming with weather and soil monitoring.
  • Transportation: Fleet management, connected cars.

2. Embedded Systems:

Definition:
An embedded system is a dedicated computer system designed to perform specific tasks within a larger system.

Key Features:

  • Task-Specific: Optimized for specific functions like control, monitoring, or processing.
  • Real-Time Operation: Often operates in real-time to meet critical timing constraints.
  • Compact and Power-Efficient: Designed with limited resources in mind.

Components:

  1. Microcontrollers/Microprocessors: The "brains" of the system (e.g., Arduino, Raspberry Pi, STM32).
  2. Peripherals: Sensors, actuators, and communication modules.
  3. Software (Firmware): Customized code running on the embedded hardware.
  4. Power Supply: Ensures reliability in energy-constrained environments.

Applications:

  • Consumer Electronics: TVs, washing machines, and gaming consoles.
  • Automotive: Engine control units (ECUs), airbags, and infotainment systems.
  • Medical Devices: Pacemakers, insulin pumps.
  • Aerospace: Flight control systems, navigation.
  • IoT Devices: Smart sensors and hubs.

3. The Convergence of IoT and Embedded Systems:

IoT relies heavily on embedded systems to function. Every "thing" in IoT is essentially an embedded system with added connectivity and software intelligence.

How They Work Together:

  • IoT Devices as Embedded Systems: IoT sensors, actuators, and controllers are essentially embedded systems with communication capabilities.
  • Data Handling: Embedded systems in IoT devices process raw data locally before transmitting it to the cloud or edge servers.
  • Edge Computing: Embedded systems enable local decision-making in IoT (e.g., real-time anomaly detection).
  • Firmware Updates: IoT enables remote updates to the firmware of embedded systems, ensuring they are up-to-date and secure.

Challenges in Integration:

  • Energy Efficiency: Power management in embedded IoT devices is critical for longevity.
  • Security: Embedded IoT systems are vulnerable to cyber threats.
  • Scalability: Integrating large numbers of devices into a cohesive IoT ecosystem.

4. Emerging Trends:

  1. AI in IoT: AI-enabled embedded systems for predictive analytics and autonomous actions.
  2. 5G and IoT: Ultra-low latency and high-speed communication for IoT applications.
  3. Low-Power Wide-Area Networks (LPWAN): Enhanced battery life for IoT sensors.
  4. IoT Security Frameworks: Advanced cryptographic and hardware-based security for embedded IoT systems.

Technical Debt

  What Is Technical Debt? Technical debt  is the future cost you incur when you choose a faster or easier solution now instead of a better l...