Technical Debt
Technical debt is often viewed solely as a negative consequence of poor engineering. However, at ttoss, we view technical debt as a financial instrument: leverage. When used consciously, it allows us to ship faster and learn earlier. When accumulated unconsciously, it becomes entropy that grinds development to a halt.
This guideline outlines our strategy for managing technical debt, grounded in The Governance of Technical Debt.
The Philosophy of Managed Debt
We accept technical debt when it buys us speed of learning or market timing, provided that the debt is:
- Visible: We know it exists.
- Contained: It doesn't infect the entire system.
- Recoverable: We have a plan to pay it down or discard it.
Unmanaged debt—sloppy code written without intent or boundaries—is not leverage; it is negligence.
Systemic vs. Modular Debt
We distinguish between Modular Debt (acceptable) and Systemic Debt (unacceptable).
Defining Systemic Technical Debt
Systemic Technical Debt is complexity that permeates the core data models, fundamental architecture, or shared business logic. It creates tight coupling between unrelated components, meaning a change in one area causes regressions in another. Unlike modular debt, systemic debt cannot be paid down incrementally; it often requires a full system rewrite.
Avoiding Systemic Debt
To ensure debt remains modular and recoverable:
- Protect the Core: Never compromise the integrity of your core domain models or database schemas for the sake of a quick UI fix.
- Strict Boundaries: Enforce unidirectional data flow and strict architectural boundaries.
- Debt at the Edges: Push "messy" code to the edges of the system (UI components, specific API adapters, scripts). Keep the center (Business Logic) clean.
Strategies for Managing Debt
To ensure technical debt remains a tool rather than a trap, we apply the following strategies:
1. Modularization and Componentization
Aligns with: The Principle of Contractual Specialization
We structure our codebase into small, independent packages, modules, and components.
- Why: If a specific module is written quickly and becomes "messy," its boundaries prevent that mess from leaking into the rest of the system.
- Strategy: Prefer many small packages over large monoliths. If a package becomes unmaintainable, it should be cheap to rewrite or replace entirely because its surface area is small.
2. Automated Verification for Every Change
Aligns with: The Corollary of Intrinsic Verification
We never trade speed for correctness. Even "quick and dirty" code must be verified.
- Why: Invisible debt is the most dangerous kind. If code is messy but tested, we can refactor it safely. If it is messy and untested, it is a landmine.
- Strategy:
- Every Pull Request must include automated tests (Unit or E2E) covering the new functionality.
- CI/CD pipelines must pass before merging.
- "Hotfixes" must be accompanied by a regression test.
3. Isolation of Business Logic
Aligns with: The Principle of Execution Isolation
We protect our core domain logic from the volatility of external tools, frameworks, and UI libraries.
- Why: Frameworks change, and UI trends shift. Your core business rules should not break when you upgrade a library.
- Strategy: Use adapters, hooks, or service layers to decouple "what the app does" (Business Logic) from "how it does it" (Implementation Details).
4. Observability as Interest Payments
Aligns with: The Principle of Invisible Risk
If we choose to ship a sub-optimal solution to move fast, we must pay the "interest" in the form of higher observability.
- Why: We need to know immediately if our "hack" fails in production.
- Strategy: Add detailed logging, metrics, and alerts around debt-heavy areas. If you can't afford to monitor it, you can't afford to ship it.
5. Atomic State Decomposition
Aligns with: The Principle of Atomic Debt Containment
Break complex workflows into discrete, atomic steps.
- Why: It allows us to isolate "messy" logic to a single step in a process.
- Strategy: Design workflows as state machines or pipelines where each step has clear inputs and outputs. This makes it easy to swap out a specific step's implementation without rewriting the whole flow.
Agent Instruction Example: Code Review
When using an AI agent to review code, you can use the following instruction template to ensure technical debt is managed effectively (change as needed for your context):
**Role**: Senior Technical Reviewer
**Objective**: Review the provided code changes to ensure that any introduced technical debt is visible, contained, and verified.
**Instructions**:
1. **Critical Errors**: If you identify critical errors, provide a specific code suggestion to fix them.
2. **Checklist Evaluation**:
- For each item in the checklist below, determine if the condition is met.
- If met, mark as `[x]`.
- If NOT met, mark as `[ ]` and describe the missing part.
3. **Output**:
- Output the checklist with your evaluation.
- If all items are `[x]`, add "LGTM".
- Do not add unnecessary comments.
**Review Checklist**:
- [ ] **Verification**: The change includes automated tests covering new functionality and edge cases.
- [ ] **Containment**: The new logic is modular and does not leak implementation details into business logic.
- [ ] **Isolation**: If this is a "quick fix", it is isolated enough to be rewritten later without side effects.
- [ ] **Observability**: If the code is complex, there are sufficient logs/metrics to detect failure.