4.1 Code complexity metrics
Overview and motivation
Cyclomatic complexity,
introduced by Thomas J. McCabe in 1976, counts the number of independent
paths through a piece of code’s control flow: each if, loop, and branch
adds to the count. It remains the most widely used code complexity metric
nearly fifty years later, alongside relatives like cognitive complexity
(which weights nested and hard-to-follow control flow more heavily than
McCabe’s original linear count) and nesting depth. These metrics share a
genuine, validated insight: code with more independent paths through it is
harder to fully test, harder to reason about, and, in decades of empirical
research, measurably more likely to contain defects.
This topic treats that insight with real respect while also treating its limits with equal seriousness. Complexity metrics measure one specific property of code, and a codebase can be simple by every complexity metric while still being poorly designed, badly named, or conceptually incoherent in ways no branch-counting algorithm can detect. Conversely, some irreducibly complex problems genuinely require complex code to solve correctly, and a team pressured to minimise a complexity score can produce code that scores well while actually being harder to understand, spreading essential complexity across more files and layers of indirection rather than reducing it.
For large teams, complexity metrics earn their keep as a triage tool: a way to find, among thousands of files, the small subset most likely to reward a closer look, not as a standalone verdict on code quality. Enterprise and government organisations maintaining codebases too large for any individual to have read in full depend on this triage function to direct scarce refactoring and review effort where it will do the most good.
Key principles
- Complexity metrics predict testing and defect difficulty; they do not measure quality directly. Treat them as one input, not a verdict.
- A complexity score is exposed to gaming through obfuscation, not just genuine simplification. Splitting complexity across more files can lower the score without actually making the code easier to understand.
- Some complexity is essential, not accidental. A genuinely hard problem may require genuinely complex code; the goal is minimising accidental complexity, not eliminating all complexity indiscriminately.
- Use complexity metrics for triage, not as an individual or team scorecard. They point at where to look, not at whom to blame.
- Trend and outliers matter more than any absolute threshold. A rising trend or an extreme outlier is more actionable than a single team-wide average.
Recommendations
Use complexity metrics to triage review and refactoring effort
Run complexity analysis across the codebase and use the results to prioritise where a closer human review or a refactoring investment would pay off most: functions or files scoring far above the codebase’s own typical range are the highest-value places to look first. This triage use, finding where to look, is complexity metrics’ most defensible and valuable application, far more so than using them as an absolute pass/fail gate.
Set thresholds relative to your own codebase, not a universal number
Absolute complexity thresholds borrowed uncritically from industry convention (a complexity score of ten is a commonly cited rule of thumb) can be either too lenient or too strict depending on your domain: a parser or a rules engine may have legitimately higher baseline complexity than a typical CRUD service. Calibrate your own thresholds against your codebase’s actual distribution, and treat a threshold breach as a prompt to look closer, not an automatic build failure, unless your team has deliberately chosen that stricter policy with full awareness of its trade-offs.
Watch for gaming through decomposition without genuine simplification
The most common way complexity scores get gamed is topic 1.2’s substitution pattern applied to this specific metric: splitting one genuinely complex function into several smaller functions that individually score well, while the overall system remains just as hard to understand, or sometimes becomes harder, because the logic is now scattered across more files with more indirection between them. Pair complexity metrics with a qualitative review of whether decomposition genuinely clarified the code, or whether it just moved the complexity somewhere the metric could no longer see it.
Distinguish essential complexity from accidental complexity before
reacting
Before treating a high complexity score as a problem to fix, ask whether the underlying problem genuinely requires that many independent paths, tax code calculation logic legitimately has many branches, for example, or whether the complexity comes from avoidable causes: deeply nested conditionals that could be flattened, duplicated logic that could be consolidated, or unclear responsibility boundaries that could be redrawn. Only the second category is a genuine quality problem this metric should drive you to fix.
Track trend and outliers, not just a snapshot average
A codebase-wide average complexity score moving slightly is rarely actionable on its own; a specific file’s complexity rising sharply over several changes, or a small number of extreme outliers in an otherwise well-behaved codebase, are far more useful signals. Track both the trend over time and the outlier tail, and use them to trigger a specific, targeted investigation rather than a broad, unfocused complexity-reduction initiative.
Trade-offs: pros and cons
| Approach | Pros | Cons |
|---|---|---|
| Absolute universal threshold | Simple, consistent, easy to automate | Ignores legitimate domain differences; can be gamed by decomposition |
| Codebase-relative threshold | Better calibrated to actual context | Requires more setup and periodic recalibration |
| Complexity as an automated build gate | Enforces consistency without human review overhead | Can block legitimately complex but well-designed code, or reward obfuscated decomposition |
| Complexity as a triage signal for human review | Catches genuine quality problems that decomposition alone would miss | Requires more human review time than a fully automated gate |
The central tension is automation versus judgement. A fully automated complexity gate is cheap to enforce and consistent, but it can both block legitimately complex, well-designed code and reward superficial decomposition that games the score without genuinely simplifying anything. Resolve the tension by using automated complexity analysis to surface candidates for review, and reserving the actual judgement, is this complexity essential or accidental, did this refactor genuinely clarify or just relocate the complexity, for a human reviewer rather than a hard automated gate alone.
Questions to discuss with your team
Are our complexity thresholds calibrated to our own codebase’s actual distribution, or borrowed uncritically from a generic industry convention? Pull your codebase’s real complexity distribution and check whether your current thresholds make sense against it, rather than assuming a commonly cited number applies universally to your domain.
Have we ever seen a function split into several smaller ones without the resulting code actually becoming easier to understand? This is the clearest sign of the decomposition-gaming pattern this topic warns about. Look at a recent refactor motivated primarily by a complexity score and assess honestly whether it improved genuine understandability.
Where in our codebase is complexity essential to the problem, and where is it accidental and fixable? Walk through your highest-complexity outliers and sort them into these two categories explicitly, since only the second category represents a genuine, actionable quality problem.
Do we use complexity metrics to triage review effort, or as a hard automated gate with no human judgement involved? Discuss whether your current enforcement approach leaves room for the essential-versus-accidental distinction this topic recommends, or whether it treats every breach identically regardless of context.
Has a complexity score ever been used, even informally, to judge an individual engineer’s work quality? This risks the same individual-evaluation trap topic 3.4 warns against for activity metrics, applied here to code metrics instead, and it invites the same gaming response.
What does our complexity trend look like over the last year for our most critical, most frequently changed files? Combine this with the churn and hotspot analysis from topic 4.3, since a file that is both highly complex and frequently changed deserves attention well before one that is complex but rarely touched.
Sector lens
Startup. Complexity metrics are usually less urgent at this scale; codebase size is small enough that informal familiarity often substitutes for formal measurement. The habit worth adopting early is simply running a complexity scan occasionally to catch a specific file quietly becoming unmanageable before the team has grown too large to notice informally.
Small business. Most modern static-analysis tools report complexity metrics as part of a broader, free or low-cost linting setup; use the output as a periodic triage signal rather than investing in dedicated tooling. Focus attention on your most frequently modified files first.
Enterprise. Complexity metrics at scale are most valuable combined with churn data (topic 4.3) to prioritise refactoring investment across a codebase too large for any individual to survey manually. Calibrate thresholds per service or domain rather than applying one organisation-wide number, since legitimate complexity varies significantly across different kinds of systems.
Government. Long-lived government systems often accumulate complexity gradually over years or decades of incremental requirement changes, and a complexity audit can be a persuasive, concrete tool for justifying modernisation or refactoring investment to stakeholders who might otherwise see the system as simply “working” and therefore not worth investing in.
Examples
Enterprise. A payments processing company ran a codebase-wide complexity audit for the first time and found a single transaction-validation function with a cyclomatic complexity score more than ten times the codebase’s median. Investigation found the complexity was almost entirely accidental: years of incrementally added special-case handling for specific payment providers had accumulated into deeply nested conditionals that could be restructured into a cleaner strategy pattern separating provider-specific logic. The refactor, prioritised directly because the complexity audit identified it as the single highest-value target in the codebase, reduced the function’s complexity score by more than 80% and, more importantly, reduced the defect rate in that specific code path measurably over the following two quarters.
Government. A tax authority’s decades-old benefits-calculation engine scored extremely high on complexity metrics across nearly every function, prompting an initial assumption that the whole system needed a ground-up rewrite. A closer, function-by-function review distinguishing essential from accidental complexity found that most of the complexity genuinely reflected the underlying legal rules, which really did have that many legitimate branches and special cases mandated by statute, while a smaller subset came from avoidable duplication across similar calculation paths. The team targeted only the accidental-complexity subset for refactoring, avoiding a costly, risky full rewrite while still meaningfully improving the system’s most genuinely problematic areas.
Business case: motivations, ROI, and TCO
The return on using complexity metrics well is targeted, high-value refactoring investment: the payments company example above shows a single, well-targeted fix, identified through complexity analysis, that measurably reduced defects in exactly the highest-risk code path, at a fraction of the cost a broad, untargeted refactoring initiative would have required.
The total cost of ownership is low: most modern development toolchains compute complexity metrics automatically as part of static analysis (topic 4.4), and the real investment is the human judgement time to interpret results correctly, distinguishing essential from accidental complexity and catching decomposition gaming, rather than any significant new tooling cost.
Anti-patterns and pitfalls
- Treating a complexity score as a direct quality verdict: it measures one specific property, not overall code quality.
- Splitting a function to game the score without genuine simplification: the decomposition-gaming pattern this topic names specifically.
- Applying a universal threshold without calibrating to your own codebase: produces either too-lenient or too-strict enforcement depending on domain.
- Using complexity metrics to individually evaluate engineers: invites gaming and misapplies a metric meant for triage, not judgement.
- Treating all complexity as equally fixable: essential complexity from a genuinely hard problem is not a defect to eliminate.
- Ignoring trend and outliers in favour of a flat, codebase-wide average: misses the most actionable signal this metric family provides.
Maturity model
- Level 1, Initiate: Complexity is not measured, or is measured with an unexamined, generic universal threshold applied uncritically.
- Level 2, Develop: Complexity metrics are collected but rarely acted on, and there is no distinction made between essential and accidental complexity.
- Level 3, Standardise: Thresholds are calibrated to the codebase’s own distribution, and complexity metrics consistently drive review and refactoring triage organisation-wide.
- Level 4, Manage: Complexity trend and outliers are actively monitored and combined with churn data (topic 4.3) to prioritise refactoring investment; decomposition gaming is actively watched for.
- Level 5, Orchestrate: The organisation can point to specific, measurable defect-rate improvements traced directly to complexity-informed refactoring investment, and complexity data is a routine, trusted input to engineering investment decisions.
Ideas for discussion
- What is our single most complex function or file, and is its complexity essential or accidental?
- Have we ever gamed a complexity score through decomposition without real simplification?
- Are our thresholds calibrated to our own codebase, or borrowed uncritically?
- Where does high complexity overlap with high churn in our codebase right now?
- Has complexity data ever informed a refactoring investment decision, or does it sit unused?
Key takeaways
- Complexity metrics like cyclomatic complexity predict testing and defect difficulty; they do not measure overall code quality directly.
- Distinguish essential complexity (from a genuinely hard problem) from accidental complexity (avoidable through better design) before reacting to a high score.
- Watch for decomposition gaming: splitting code to lower a score without genuinely simplifying anything.
- Use complexity metrics for triage, directing human review and refactoring effort, not as an individual scorecard or a rigid automated gate.
- Calibrate thresholds to your own codebase’s distribution, and track trend and outliers, not just a flat average.
References and further reading
- McCabe, Thomas J., “A Complexity Measure,” IEEE Transactions on Software Engineering (1976): the original cyclomatic complexity paper.
- Code Complete, by Steve McConnell (practical guidance on managing complexity in software construction).
- Working Effectively with Legacy Code, by Michael Feathers (techniques for safely reducing complexity in existing, hard-to-change code).
- Campbell, G. Ann, “Cognitive Complexity: A New Way of Measuring Understandability” (SonarSource, 2018): the cognitive complexity metric and its distinction from cyclomatic complexity.