software_engineering/technical_debt.rs
1//! # Technical Debt Measurement
2//!
3//! **Technical debt**, a metaphor coined by Ward Cunningham, describes the
4//! accumulated cost of past shortcuts — expedient decisions that shipped
5//! something sooner but left the codebase harder to change afterward, in
6//! the same way financial debt lets you spend now at the cost of interest
7//! later. Unmeasured debt loses the prioritization competition against
8//! feature work by default, not because it matters less, but because it
9//! has no visible advocate; quantifying it in terms decision-makers can
10//! weigh — cost to fix versus cost of carrying it — is what lets it compete
11//! fairly.
12//!
13//! ## Formula
14//!
15//! ```text
16//! Debt carrying cost = (velocity tax + elevated defect cost) × periods
17//!
18//! velocity tax = extra cost per period from related work going slower
19//! elevated defect cost = extra expected defect cost per period from carrying the item
20//! periods = number of periods the item is left unfixed
21//! ```
22//!
23//! ## Why it matters
24//!
25//! For each debt item, the chapter recommends estimating two figures: the
26//! cost to fix it, and the cost of carrying it unfixed — how much slower
27//! related work goes, how much additional defect risk it carries, how much
28//! it blocks other work. This carrying cost, summed across however many
29//! periods the item is left unaddressed, gives decision-makers a real basis
30//! for comparison against feature work's cost and expected value, rather
31//! than an abstract, unquantified complaint. Debt also compounds: each new
32//! shortcut makes the next change slightly harder.
33//!
34//! ## Example
35//!
36//! The topic doc's enterprise example: a telecommunications company
37//! allocated a fixed 15% of engineering capacity to debt remediation and
38//! resolved its top five highest-carrying-cost items within a year,
39//! measurably improving change failure rate for billing-related deploys —
40//! the return on quantifying and targeting the highest-carrying-cost items
41//! first.
42//!
43//! ```rust
44//! use software_engineering::technical_debt::debt_carrying_cost;
45//!
46//! // A billing-engine shortcut: slower related work (velocity tax) plus
47//! // elevated defect risk, both recurring per period, carried for a year
48//! // (12 monthly periods) before remediation.
49//! let cost = debt_carrying_cost(2_000.0, 500.0, 12.0);
50//! assert_eq!(cost, 30_000.0);
51//!
52//! // Carrying the same item twice as long doubles its carrying cost.
53//! let longer = debt_carrying_cost(2_000.0, 500.0, 24.0);
54//! assert_eq!(longer, cost * 2.0);
55//! ```
56//!
57//! ## Money
58//!
59//! [`debt_carrying_cost`] takes plain `f64` amounts. For currency-checked
60//! accounting, use [`rusty_money::Money`] directly rather than through a
61//! wrapper this crate provides — its own `add`/`mul` already return
62//! `Result`, rejecting a currency mismatch (a USD velocity tax against a
63//! EUR defect cost, say) instead of silently summing incompatible amounts,
64//! so this formula needs no adapter to use it that way:
65//!
66//! ```rust
67//! use rusty_money::{Money, iso};
68//!
69//! // $2,000/month velocity tax + $500/month elevated defect cost,
70//! // carried for 12 months = $30,000.
71//! let velocity_tax = Money::from_major(2_000, iso::USD);
72//! let defect_cost = Money::from_major(500, iso::USD);
73//! let cost = velocity_tax.add(defect_cost).unwrap().mul(12).unwrap();
74//! assert_eq!(cost, Money::from_major(30_000, iso::USD));
75//!
76//! // Mismatched currencies are rejected rather than silently summed.
77//! let eur_defect_cost = Money::from_major(500, iso::EUR);
78//! assert!(velocity_tax.add(eur_defect_cost).is_err());
79//! ```
80//!
81//! ## Pitfalls
82//!
83//! - **No visible, tracked debt backlog** — debt loses the prioritization
84//! competition by default and compounds invisibly.
85//! - **Vague, unquantified debt claims** — rarely compete well against
86//! concrete, quantified feature requests in planning.
87//! - **Prioritizing debt by age or advocacy volume rather than impact** —
88//! misdirects limited remediation capacity away from the highest-carrying
89//! -cost items.
90//! - **No protected capacity for remediation** — debt paydown only happens
91//! reactively, after a crisis, rather than as routine, deliberate practice.
92//! - **Treating all debt as equally worth fixing**, instead of accepting
93//! some debt as permanent when its cost to fix exceeds its cost to carry.
94//!
95//! ## Sources
96//!
97//! - Chapter 4.5, Technical debt measurement.
98//! - Cunningham, Ward, "The `WyCash` Portfolio Management System," *OOPSLA*
99//! (1992).
100//! - Kruchten, Philippe, Robert Nord, and Ipek Ozkaya, *Managing Technical
101//! Debt: Reducing Friction in Software Development*.
102//!
103//! Topic doc: software-engineering-metrics/locales/en-001/chapters/04-05-technical-debt-measurement.md
104
105/// Debt carrying cost: the ongoing cost of leaving a debt item unfixed.
106///
107/// `(velocity_tax_per_period + elevated_defect_cost_per_period) × periods`.
108/// The velocity tax captures how much slower related work goes while the
109/// item is carried; the elevated defect cost captures the additional
110/// expected defect risk it carries. Summing both per period gives a figure
111/// decision-makers can weigh directly against the item's one-time cost to
112/// fix, and against competing feature work.
113///
114/// # Arguments
115///
116/// * `velocity_tax_per_period` — extra cost per period from related work
117/// going slower while the item is unfixed (any currency unit).
118/// * `elevated_defect_cost_per_period` — extra expected defect cost per
119/// period attributable to carrying the item, in the same unit.
120/// * `periods` — number of periods (e.g. months) the item is carried
121/// unfixed.
122///
123/// # Returns
124///
125/// The total carrying cost over `periods`, in the same unit as the two
126/// per-period inputs.
127///
128/// # Examples
129///
130/// ```rust
131/// use software_engineering::technical_debt::debt_carrying_cost;
132///
133/// // $2,000/month velocity tax + $500/month elevated defect cost,
134/// // carried for 12 months = $30,000.
135/// assert_eq!(debt_carrying_cost(2_000.0, 500.0, 12.0), 30_000.0);
136/// ```
137#[must_use]
138pub fn debt_carrying_cost(
139 velocity_tax_per_period: f64,
140 elevated_defect_cost_per_period: f64,
141 periods: f64,
142) -> f64 {
143 (velocity_tax_per_period + elevated_defect_cost_per_period) * periods
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 // "how much slower does related work go, how much additional defect
151 // risk does it carry" — both costs sum per period, then scale with how
152 // many periods the item is carried.
153 #[test]
154 fn carrying_cost_sums_velocity_tax_and_defect_cost_across_periods() {
155 let cost = debt_carrying_cost(2_000.0, 500.0, 12.0);
156 assert!((cost - 30_000.0).abs() < 1e-9);
157 }
158
159 // "each new shortcut makes the next change slightly harder, which
160 // creates pressure for more shortcuts, which compounds further" — the
161 // same per-period cost carried twice as long doubles the total.
162 #[test]
163 fn carrying_the_same_item_twice_as_long_doubles_its_cost() {
164 let one_year = debt_carrying_cost(2_000.0, 500.0, 12.0);
165 let two_years = debt_carrying_cost(2_000.0, 500.0, 24.0);
166 assert!((two_years - one_year * 2.0).abs() < 1e-9);
167 }
168
169 // "allocated a fixed 15% of engineering capacity to debt remediation
170 // going forward" — a zero carrying cost (no velocity tax, no elevated
171 // defect cost) is the case where an item is reasonable to leave
172 // permanently unaddressed, per the chapter's "accept some debt as
173 // permanent" recommendation.
174 #[test]
175 fn zero_velocity_tax_and_defect_cost_yields_zero_carrying_cost() {
176 assert!((debt_carrying_cost(0.0, 0.0, 12.0) - 0.0).abs() < 1e-9);
177 }
178}