software_engineering/unit_economics.rs
1//! # Cost and Unit Economics of Engineering
2//!
3//! Engineering cost has at least three distinct components with different
4//! drivers and different levers: **people cost** (salaries and benefits,
5//! largely fixed in the short term), **infrastructure cost** (cloud spend,
6//! largely variable with usage), and **tooling and licensing cost** (often
7//! fixed per-seat or per-usage-tier). Tracking them separately, rather than
8//! as one blended total, matters because a rising total driven by
9//! infrastructure scaling with genuine growth calls for a very different
10//! response than the same rise driven by unmanaged tooling sprawl. Dividing
11//! the total by a genuine unit of value delivered — cost per customer
12//! served, per transaction, per deployment — turns that total into a
13//! trackable, comparable trend.
14//!
15//! ## Formula
16//!
17//! ```text
18//! Total engineering cost = people cost + infrastructure cost + tooling cost
19//! Unit cost = total cost / units delivered
20//! ```
21//!
22//! ## Why it matters
23//!
24//! Selecting a unit that genuinely tracks business or mission value, rather
25//! than an easily inflated, internal, largely discretionary count, is what
26//! keeps a unit-cost ratio honest. A single unit-cost snapshot is also less
27//! useful than its trend: falling unit cost as the platform matures signals
28//! genuine efficiency gains, while rising unit cost is often a direct,
29//! measurable consequence of accumulated technical debt or complexity
30//! hotspots elsewhere in the codebase.
31//!
32//! ## Example
33//!
34//! ```rust
35//! use software_engineering::unit_economics::{total_engineering_cost, unit_cost};
36//!
37//! // $500k people cost, $120k infrastructure, $30k tooling, serving 10,000
38//! // customers.
39//! let total = total_engineering_cost(500_000.0, 120_000.0, 30_000.0);
40//! assert_eq!(total, 650_000.0);
41//!
42//! let cost_per_customer = unit_cost(total, 10_000.0).unwrap();
43//! assert_eq!(cost_per_customer, 65.0);
44//! ```
45//!
46//! ## Money
47//!
48//! [`total_engineering_cost`] and [`unit_cost`] take plain `f64` amounts.
49//! For currency-checked accounting, use [`rusty_money::Money`] directly
50//! rather than through a wrapper this crate provides — its own `add`/`div`
51//! already return `Result`, rejecting cost components quoted in different
52//! currencies instead of silently treating them as the same unit:
53//!
54//! ```rust
55//! use rusty_money::{Money, iso};
56//! use software_engineering::unit_economics::unit_cost;
57//!
58//! let people = Money::from_major(500_000, iso::USD);
59//! let infrastructure = Money::from_major(120_000, iso::USD);
60//! let tooling = Money::from_major(30_000, iso::USD);
61//! let total = people.add(infrastructure).unwrap().add(tooling).unwrap();
62//! assert_eq!(total, Money::from_major(650_000, iso::USD));
63//!
64//! let cost_per_customer = unit_cost(total.to_f64_lossy(), 10_000.0).unwrap();
65//! assert!((cost_per_customer - 65.0).abs() < 1e-9);
66//!
67//! // Dividing by zero units is rejected rather than producing infinity.
68//! assert!(total.div(0).is_err());
69//! ```
70//!
71//! ## Pitfalls
72//!
73//! - **Choosing an easily inflated denominator** that does not correspond to
74//! any genuine external unit of value delivered — flatters the ratio
75//! without informing anyone.
76//! - **Tracking one blended cost total** instead of separating people,
77//! infrastructure, and tooling cost — hides which lever actually needs
78//! pulling when the total rises.
79//! - **Reporting a unit-cost snapshot with no trend** — a single number
80//! says nothing about whether efficiency is improving or degrading.
81//! - **Never connecting rising unit cost back to technical debt or
82//! complexity metrics** — misses a measurable, quantifiable case for debt
83//! remediation investment.
84//!
85//! ## Sources
86//!
87//! - Chapter 5.4, Cost and unit economics of engineering.
88//! - `FinOps` Foundation, *`FinOps` Framework*.
89//!
90//! Topic doc: software-engineering-metrics/locales/en-001/chapters/05-04-cost-and-unit-economics-of-engineering.md
91
92/// The sum of engineering's three distinct cost components.
93///
94/// `people_cost + infrastructure_cost + tooling_cost`. This function is
95/// only the sum used to compute a unit cost; callers should keep tracking
96/// the three components separately elsewhere, per the chapter's explicit
97/// recommendation, rather than discarding the breakdown once summed.
98///
99/// # Arguments
100///
101/// * `people_cost` — salaries and benefits, in any currency unit.
102/// * `infrastructure_cost` — cloud and infrastructure spend, in the same
103/// unit.
104/// * `tooling_cost` — tooling and licensing cost, in the same unit.
105///
106/// # Returns
107///
108/// The total engineering cost, in the same unit.
109///
110/// # Examples
111///
112/// ```rust
113/// use software_engineering::unit_economics::total_engineering_cost;
114///
115/// let total = total_engineering_cost(500_000.0, 120_000.0, 30_000.0);
116/// assert_eq!(total, 650_000.0);
117/// ```
118#[must_use]
119pub fn total_engineering_cost(people_cost: f64, infrastructure_cost: f64, tooling_cost: f64) -> f64 {
120 people_cost + infrastructure_cost + tooling_cost
121}
122
123/// Cost per genuine unit of value delivered, such as cost per customer
124/// served, per transaction, or per deployment.
125///
126/// `total_cost / units_delivered`.
127///
128/// # Arguments
129///
130/// * `total_cost` — total engineering cost for the period, in any currency
131/// unit (typically from [`total_engineering_cost`]).
132/// * `units_delivered` — count of genuine value units delivered in the same
133/// period.
134///
135/// # Returns
136///
137/// The cost per unit, or `None` if `units_delivered` is zero.
138///
139/// # Examples
140///
141/// ```rust
142/// use software_engineering::unit_economics::unit_cost;
143///
144/// assert_eq!(unit_cost(650_000.0, 10_000.0), Some(65.0));
145/// assert_eq!(unit_cost(1.0, 0.0), None);
146/// ```
147#[must_use]
148pub fn unit_cost(total_cost: f64, units_delivered: f64) -> Option<f64> {
149 if units_delivered == 0.0 {
150 return None;
151 }
152 Some(total_cost / units_delivered)
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 // "Engineering cost has at least three distinct components with
160 // different drivers and different levers: people cost ...
161 // infrastructure cost ... and tooling and licensing cost."
162 #[test]
163 fn total_engineering_cost_sums_the_three_components() {
164 let total = total_engineering_cost(500_000.0, 120_000.0, 30_000.0);
165 assert!((total - 650_000.0).abs() < 1e-9);
166 }
167
168 // "cost per customer served, cost per transaction processed, cost per
169 // deployment."
170 #[test]
171 fn unit_cost_divides_total_by_units_delivered() {
172 let cost_per_customer = unit_cost(650_000.0, 10_000.0).unwrap();
173 assert!((cost_per_customer - 65.0).abs() < 1e-9);
174 }
175
176 #[test]
177 fn unit_cost_is_none_for_zero_units() {
178 assert_eq!(unit_cost(1.0, 0.0), None);
179 }
180}