0

Technical debt — the future cost of choosing a quick solution now

Beginner5 min read·eng-16-019
interview

Concept

Technical debt — the accumulated cost of choosing quick, expedient solutions over better but more time-consuming ones. Like financial debt: you get something now, but pay interest later through slower development, more bugs, and higher maintenance cost.

Coined by Ward Cunningham (1992), one of the creators of Extreme Programming and Wiki. He used the debt metaphor to explain why code refactoring was necessary to business stakeholders.

Types of technical debt:

  • Deliberate debt: A conscious trade-off. "We'll hardcode this now to meet the deadline, and refactor after launch." Acceptable if the debt is documented and actually repaid.
  • Inadvertent/accidental debt: Poor practices without realizing it. Writing code that seemed correct at the time but is now known to be suboptimal.
  • Reckless debt: Knowingly taking shortcuts without planning to address them. "We don't have time for tests" without any plan to add them later.
  • Gradual/environmental debt: Code that was good when written but is now outdated (old PHP version, deprecated library, obsolete pattern).

The interest payment: Technical debt doesn't just slow you down once — you pay "interest" on it continuously. Every feature that touches the indebted code takes longer. Every bug fix is harder. New developers take longer to onboard.

Managing technical debt:

  • Track it: Add a comment, a ticket, or a TODO with context.
  • Prioritize repayment: Not all debt needs to be repaid immediately — only debt in high-traffic code paths.
  • Allocate time: Some teams reserve 20% of sprint capacity for technical debt.
  • Prevent accumulation: Code review, coding standards, automated checks.

Code Example

php
<?php
// DELIBERATE TECHNICAL DEBT — documented shortcut
class PaymentProcessor
{
    public function charge(float $amount, string $cardToken): bool
    {
        // TODO(#1234): This hardcodes USD — must support multiple currencies before EU launch.
        // Hardcoded due to time pressure for Black Friday. Repay in Q1.
        $response = Http::post('https://api.stripe.com/v1/charges', [
            'amount'   => (int)($amount * 100),
            'currency' => 'usd', // ← deliberate debt: hardcoded currency
            'source'   => $cardToken,
        ]);
        return $response->successful();
    }
}

// RECKLESS DEBT — no plan to fix, accumulates interest
class UserController extends Controller
{
    public function update(Request $request, int $id)
    {
        // No validation (reckless debt — security risk)
        // No authorization (reckless debt — security risk)
        // No type safety (reckless debt — runtime errors)
        $user = \DB::table('users')->where('id', $id)->first();
        \DB::table('users')->where('id', $id)->update($request->all()); // mass-assignment risk!
        return response()->json($user);
    }
}

// GRADUAL DEBT — was fine in PHP 7.4, now needs updating
function getUser(int $id) // no return type hint
{
    $user = \DB::table('users')->find($id);
    return $user ? (array)$user : null; // returns array or null — unclear to callers
    // Modern: should return ?UserDTO and add return type
}

// PAYING DOWN DEBT — refactoring the reckless example
class UserController extends Controller
{
    public function update(UpdateUserRequest $request, User $user): JsonResponse // proper types, validation
    {
        $this->authorize('update', $user);     // authorization added
        $user->update($request->validated()); // validated + type-safe update
        return response()->json(new UserResource($user));
    }
}

// TRACKING DEBT in code
// 1. Comments with ticket references:
// FIXME(TECH-123): This N+1 query degrades at scale — add with() before v2 launch
// 2. Static analysis tools catch some:
//    phpstan, psalm → type errors, unused code
// 3. Mutation testing → gaps in test coverage
// 4. Architecture tests → PHPArkitect, NativePHP → enforce rules
\Arkitect\Rule::allClasses()
    ->that(\Arkitect\Expressions\IsA::classname('Controller'))
    ->should(\Arkitect\Expressions\DependsOnlyOnTheseNamespaces::ofNames(['App\\Http\\Requests', 'App\\Http\\Resources']))
    ->because('Controllers should only depend on Form Requests and Resources');