← the whole book on one page

Chapter 2: The Codebase — Design, Debt, and Change

“Shipping first time code is like going into debt.” — Ward Cunningham

Every codebase begins as a record of what its authors understood on the day they wrote it. The understanding was always partial, because nobody knows a domain fully before building software for it, and it was always local to a handful of people, because a young company is a handful of people. Then the company lives on. The domain turns out to be stranger than anyone thought, new people arrive who never shared the original understanding, and the code is asked to do things its authors could not have imagined. The question of this chapter is how a codebase stays changeable through all of that: how we keep its complexity within what people can hold in their heads, how we tell which of its imperfections actually cost us something, and how we change old code that nobody fully understands and every customer passes through.

The most famous answer to part of that question was given in two pages. In 1992 Ward Cunningham was director of research and development at Wyatt Software, which built WyCASH+, a portfolio management system for cash securities, the short-dated instruments that treasurers hold for months rather than years. It was written in Smalltalk by a small team, each member of which kept, in Cunningham’s words, “general knowledge of all aspects of the roughly four megabytes of source code”. He wrote up the experience for the Object-Oriented Programming, Systems, Languages and Applications conference (OOPSLA) held in Vancouver that October, and in the middle of an otherwise modest report he set down a passage the industry has been quoting ever since: “Shipping first time code is like going into debt. A little debt speeds development so long as it is paid back promptly with a rewrite. … The danger occurs when the debt is not repaid. Every minute spent on not-quite-right code counts as interest on that debt. Entire engineering organizations can be brought to a stand-still under the debt load of an unconsolidated implementation, object-oriented or otherwise.”

Two details of that passage are easy to miss. The first is the instrument of repayment: in 1992 Cunningham wrote “a rewrite”, meaning revising and consolidating code until it matched what the team had since learned; the word “refactoring”, which his later retellings use, was only then being defined in a doctoral thesis at Illinois. The second sits two sentences earlier, where he names the cost of debt that is never repaid: immature code in excess makes a program “unmasterable, leading to extreme specialization of programmers and finally an inflexible product.” He named the human cost of debt, the knowledge that concentrates in too few heads, before the industry had a name for its cost in code. We will meet both halves of that cost at Encore.

Encore is four months into Verified Resale. The bet has been made, discovery is done, and the Resale team has formed out of people from Orders & Payments and Partner APIs. Every resold ticket must be reissued under a new barcode, and the old one revoked at every gate. That puts the work on the oldest code Encore owns. Barcode issuance was written in the first year, when the company was nine engineers in one team, and it is now about eight years old. It sits inside Seat Inventory & Ticketing and belongs to the Inventory team. One person still understands it: Lena, one of the original nine, now on the Inventory team. It has no tests worth the name, and every ticket Encore has ever sold has passed through it. This chapter follows the Inventory and Resale teams as they measure that code, get it under test, and change it, and it draws from their work the practices that keep any large codebase changeable.

2.1 Complexity and Conceptual Integrity

What complexity is

Complexity is a word engineers use loosely, often to mean nothing more than “code I did not write”. John Ousterhout, who built systems such as the Tcl language and the Sprite operating system before writing A Philosophy of Software Design, gives a definition precise enough to work with: “Complexity is anything related to the structure of a software system that makes it hard to understand and modify the system.” The definition is about structure and about people: a system is complex to the degree that those who must change it find it hard to change, whatever its size. In the terms the Architecture book took from Fred Brooks, this is mostly accidental complexity: the kind we add through the shape of our solution, as distinct from the essential complexity that the domain itself imposes.

Ousterhout sees complexity through three symptoms. The first is change amplification: a seemingly simple change requires modifications in many places. The second is cognitive load: how much a developer must know to complete a change safely. (The Architecture book used the phrase in the Team Topologies sense, for what a whole team must hold in mind; Ousterhout means the knowledge one person needs for one change.) The third symptom is the worst: unknown unknowns, where it is not obvious which code must change, or what one must know, so that a developer can follow every rule and still break something. Behind all three he finds two causes. Dependencies mean a piece of code cannot be understood or changed in isolation. Obscurity means important information is not obvious, because a name is vague, a convention is undocumented, or a value means something different in two places.

Encore’s checkout code showed all three symptoms in one change earlier this year, when an order needed to record whether it came from a resale. Adding a value to the order’s origin enumeration touched nine files across the purchase saga, the order history projection and the fan app’s receipt view: change amplification. The engineer had to know that the saga’s compensation step, which undoes a reservation when payment fails, reads the same enumeration to decide whether seats return to the public or to a seller: cognitive load. And nothing in the code said that the Finance platform team’s nightly reconciliation grouped orders by origin, so resale orders would have dropped out of it had a reviewer from that team not happened to see the pull request: an unknown unknown. Figure 2.1 sets the symptoms beside their causes.

Symptom What it feels like Typical cause Encore’s example
Change amplification One idea, many edits Dependencies duplicated across modules A new order origin touched nine files
Cognitive load Much to know before a small change Leaky interfaces, shared enumerations The compensation step reads the same origin
Unknown unknowns Nothing says what else must change Obscurity: an undocumented consumer Finance’s reconciliation grouped by origin

Figure 2.1 — Ousterhout’s three symptoms of complexity and their causes, with the order-origin change at Encore. The third row is the dangerous one, because it does not announce itself.

Ousterhout adds that “Complexity is more apparent to readers than writers.” The author of a change knows why each line is there, while the reader six months later has only the code, so the best judges of a change’s complexity are the people who must change it next.

Deep modules

If dependencies and obscurity cause complexity, the main tool against it is the module that hides things. Parnas made the decisive argument in 1972, that a module should be defined by the design decision it hides, and the Architecture book’s Section 1.3 develops it, so we take it as given. What Ousterhout adds is a test we can apply to a single class or library. A deep module is one whose interface is small compared with the functionality it provides; its depth is the ratio between what it does for us and what we must know to use it. His favorite example is Unix file input and output. Five calls, open, read, write, lseek and close, stand in front of an implementation that manages disk layout, caching, permissions, devices and concurrent access, and a program can use files for years without learning any of that. A shallow module has an interface nearly as complicated as its implementation, so it costs almost as much to learn as it saves.

Shallow modules multiply in codebases that follow “small classes, small methods” as a rule. Ousterhout calls the condition “classitis” and describes it plainly: “Classitis may result in classes that are individually simple, but it increases the complexity of the overall system.” Each class adds an interface to learn and a boundary to cross, and a reader chasing one behavior through seven three-line classes has paid seven times for something one class could have hidden. This is contested ground: in 2024 Ousterhout and Robert C. Martin published a long written debate on method length, comments and decomposition, in which two experienced designers disagree in good faith. What survives the debate is the test itself: for each module, compare what its users must know with what it spares them.

A theater box office is a good picture of a deep module. The fan sees a window and a person; behind them sit the seating plan, the promoter’s holds, the house seats, the accessible places, the band’s guest list and the rules about each. The fan asks for two seats and gets two seats. The analogy breaks where software differs from people: a clerk can explain an exception in words, while a module that hides too much leaves its callers baffled when something unusual happens, which is why depth is measured as a ratio.

Applied to Encore’s barcode issuer, the test gave an uncomfortable answer. Its public surface had grown to 23 methods over eight years, several of them variants of one another with a flag for a festival pass, a box-office sale or a ticket from before the year-four split into services. Callers chose the variant, so every caller had to understand the distinctions the issuer should have hidden, and that shallowness was one reason its last author was consulted on so many changes.

Pulling complexity downward

A module’s author usually understands its hard cases better than any caller does, and there is one author and many callers. Ousterhout draws the consequence: it is better for a module to absorb a difficult case once, inside, than to make every caller deal with it. He calls this pulling complexity downward. Its sharpest form is what he calls defining errors out of existence, which means designing an operation so that a situation that would otherwise be an error is simply part of its normal meaning. Unix, in his best example, lets a program delete a file that another program has open: the name disappears at once and the storage is reclaimed when the last user closes it, whereas Windows long refused such deletions and pushed the problem back onto users and programs.

The everyday version of this idea is idempotence. The Architecture book’s Section 4.1 gave the network reason for it: any operation that crosses a network will be retried, so it must be safe to receive twice. The design reason is just as strong. An idempotent operation removes a whole class of error paths from every caller, because a caller that is unsure whether its request arrived can simply send it again.

The Reissue operation that Verified Resale needs shows both the power of the idea and its limit. A resale purchase triggers a reissue, and the caller may time out and retry; if the retry produced a second new barcode, the buyer would hold two tickets. So a retried reissue request carrying the same idempotency key returns the original result, and every caller is spared the question of whether its first attempt landed. It would be easy to go further and say that reissuing a ticket twice is a no-op, and that would be wrong, because a ticket bought on resale can legitimately be resold again, and the second resale needs a second reissue under a new identity. Ousterhout is careful here too: an error should be defined away only when the caller has nothing to decide. A request naming a ticket identity already revoked by another resale is a genuine conflict about which the caller must tell the buyer something, so the interface returns it. The Inventory team’s interface came out at a dozen lines:

/** Reissues a sold ticket to a new holder under a new, opaque identity.
 *  The old identity is revoked at every gate in the same transaction. */
interface Reissue {
    fun reissue(request: ReissueRequest): ReissueOutcome
}

data class ReissueRequest(
    val idempotencyKey: IdempotencyKey, // one per resale; a retry reuses it
    val ticketId: TicketId,             // the identity the seller holds now
    val newHolder: FanId,
    val resaleId: ResaleId,
)

sealed interface ReissueOutcome {
    data class Reissued(val newTicketId: TicketId, val revoked: TicketId) : ReissueOutcome
    data class Conflict(val currentTicketId: TicketId) : ReissueOutcome // already resold
}

Everything a caller does not need to decide, from check digits to revocation lists, stays behind that interface, and the one thing a caller must decide is returned as a value it cannot ignore.

Strategic and tactical programming

Why do codebases accumulate shallow modules and leaked cases in the first place? Ousterhout’s answer is a habit of mind he calls tactical programming: the goal is to get the next feature working, and any design thought that does not serve that goal is postponed. Each postponement is small and defensible, and their sum is what the next team inherits. He describes its extreme form, the “tactical tornado”, a prolific programmer who turns out features faster than anyone else and leaves the code a little worse with each one; “They are rarely considered heroes by the engineers who must work with their code in the future.” The opposite habit, strategic programming, treats working code as necessary but insufficient, and treats the structure of the system as the main thing being built.

Ousterhout puts a number on the habit: “I suggest spending about 10–20% of your total development time on investments.” The figure is a seasoned designer’s advice, without measurement behind it, and since he expects the investment to pay back “within a few months”, it fits code that will be thrown away sooner poorly. Its main use is as a standard against which a team can notice that its real figure is zero. David Farley makes the broader point in Modern Software Engineering (2021): engineering, as he frames it, means becoming expert at two things, at learning and at managing complexity, and the second is impossible for a team that never spends time on structure.

It would be unfair to call Encore’s year-one barcode code tactical in this sense. Nine engineers wrote it to fit what they understood about tickets, and they understood a ticket as something issued once to one fan and scanned once at one door. Nobody then imagined a ticket changing owners. The code is debt in Cunningham’s sense, faithful to an understanding that has since moved on. Code written carelessly calls for a conversation about habits; code written well for a world that has since changed calls for consolidation, and for the help of its authors, who know best what the world used to be.

Conceptual integrity across forty teams

Brooks wrote in The Mythical Man-Month: “I will contend that conceptual integrity is the most important consideration in system design.” He meant that a system should reflect one set of design ideas, so that a person who has learned one part can predict the rest, and he argued that such integrity comes from a few minds. That is easy at nine engineers, where the few minds are the whole company. At 400 engineers in 40 teams it cannot be done literally, and any attempt to route every design question through a small group of architects produces a queue, which the Architecture book’s last chapter argued against at length.

The mechanisms that do work at scale are humbler than a chief architect, and Software Engineering at Google describes them well in its chapter on style guides. A rule earns its place only if it pulls its weight; the rules optimize for the reader rather than the writer; they favor consistency; they forbid constructs that are error-prone or surprising; and they concede to practicalities. Two lines from that chapter capture the spirit: “It’s the consistency of having one answer rather than the answer itself that is the valuable part,” and “Documenting the reasoning behind a given decision gives us the advantage of being able to recognize when things need to change.” A rule with its reason beside it can be argued with and retired when the reason lapses; a rule without one becomes folklore.

Publishing has long worked this way. A publishing house keeps a style sheet, often built on a reference such as The Chicago Manual of Style, so that many authors and copy editors produce books that read as if one careful person had prepared them all, and readers spend their attention on the argument and none on the commas. The analogy breaks at enforcement. A copy editor reads every page, while no person can read every change at Encore, so Encore’s style guides, one per language, each with an owning group and a reason beside every rule, are enforced wherever possible by formatters and linters that run before a human reviewer sees the code. The mechanics of owners and automated review belong to Sections 4.2 and 4.3. To these rules we add the habit Ousterhout calls “design it twice”: sketching a second, different design before committing to the first, which is cheap and often shows the first design’s weaknesses while they are still cheap to fix.

Recap. Complexity is whatever in a system’s structure makes it hard to understand and change, felt as change amplification, cognitive load and unknown unknowns, and caused by dependencies and obscurity. Deep modules, which hide much behind a small interface, and operations that absorb hard cases once instead of in every caller, are the main defenses. At forty teams, conceptual integrity survives through written rules that carry their reasons and are applied by tools.

Exercise 2.1. Pick one module your team owns. List its interface, then list what it hides. Is it deep or shallow? Name one error its callers handle that the module could define away, and one it must keep returning because callers have a real decision to make.

2.2 Refactoring and Tidying

Behavior-preserving change in small steps

Design is never finished, so most design happens to code that already exists. The discipline for doing it safely has a name and a lineage. Refactoring is a change to the internal structure of software that makes it easier to understand or cheaper to modify without changing its observable behavior. William Opdyke gave the idea its first thorough academic treatment in his 1992 doctoral thesis at the University of Illinois, Refactoring Object-Oriented Frameworks, written under Ralph Johnson in the same year as Cunningham’s experience report, and Martin Fowler’s Refactoring (1999, second edition 2018) turned it into a working vocabulary. Each refactoring in Fowler’s catalog, such as Extract Function, Move Field or Replace Conditional with Polymorphism, is a named sequence of small steps, each of which leaves the code compiling and its tests passing.

The value of small steps is that each can be checked. A large restructuring done in one stroke may be correct, but nobody can tell; a sequence of named steps can be verified one at a time, by the tests and by a reviewer who recognizes the name. Kent Beck put the strategy into one sentence: “for each desired change, make the change easy (warning: this may be hard), then make the easy change.” It divides work into a structural part and a behavioral part, and suggests that the first is often the larger. Named refactorings also make changes legible to reviewers when a machine has made them, which matters more now that assistants propose so many of them, a matter Chapter 5 takes up.

The evidence that refactoring pays is real but qualified. Miryung Kim, Thomas Zimmermann and Nachiappan Nagappan surveyed engineers at Microsoft in 2012 and found that they used the word far more loosely than “behavior-preserving”, and that many saw refactoring as risky. In Windows 7, the modules a dedicated team had refactored ended up with fewer dependencies between modules and fewer defects after release. The authors’ later journal version qualifies that result, and we should hold it accordingly: deliberate refactoring of code that matters has measurable benefits, while the loose sense of the word promises nothing.

Two hats

Fowler credits Beck with the metaphor of two hats. When we add behavior, we wear one hat: we add tests and make them pass, and we do not restructure. When we refactor, we wear the other: we change structure, add no behavior, and expect every existing test to keep passing. We may swap hats every few minutes, but we wear only one at a time. Beck’s Tidy First? (2023) carries the idea into how changes are submitted: structure changes and behavior changes go into separate pull requests, so that each can be reviewed for what it is.

Publishing again offers the picture. A structural edit moves chapters, cuts sections and changes an argument’s order; a copy edit corrects grammar and house style. A good editor does not do both in one pass on the same pages, because an author reviewing the marked-up manuscript cannot tell whether a changed sentence was moved, cut or corrected, and so cannot judge any of the changes well. The same is true of a reviewer facing a pull request that renames 40 identifiers and also changes how refunds are calculated. The refund change, which is the one that matters, hides in the noise of the renames.

The cost of mixing reaches further than review. Kim Herzig and Andreas Zeller studied bug fixes in five Java projects in 2013 and found that up to 15% of them were tangled with unrelated changes, which caused at least 16.6% of source files to be wrongly associated with bugs by the tools that mine version history. Tangled commits damage the history itself, and every analysis built on that history, including the hotspot and coupling analyses of Section 2.3, inherits the damage. Encore’s review guide therefore asks for structure and behavior in separate pull requests on the purchase path, and gives its reason in one line: so that a reviewer can see the behavior change on its own.

Tidyings

Not every structural change deserves the word refactoring. Beck’s book names a smaller unit. A tidying is a structural change small enough to be done in minutes and safe enough to need no special justification. His catalog is modest on purpose: guard clauses that return early instead of nesting, deleting dead code, normalizing symmetries so that similar things look similar, reading order and cohesion order, explaining variables and explaining constants, explicit parameters, chunking statements, extracting a helper, and deleting comments that repeat the code. Their point is to make the next change easier to read, and they are done just before it, by the person about to make it.

Dead code is the tidying people most often skip, because it seems harmless: it is not called, so it cannot hurt. The Resilience book’s Chapter 4 tells how Knight Capital lost more than $460 million in about 45 minutes in 2012 when a repurposed flag woke code that had been unused for years, so dead code carries a risk of its own, which deleting it removes.

The Inventory team’s first changes to the barcode issuer were tidyings of exactly this kind. The issuer computed its check digit with three unexplained integers; an explaining constant named each one, and Lena, reading the pull request, wrote a sentence beside the constants explaining which scanner vendor’s firmware had required them in year one. Two of the 23 public methods had had no callers since the year-four split, and were deleted. A deeply nested branch for festival passes became three guard clauses. Each pull request took less than an hour and changed no behavior, and together they turned a file that only one person could read into one that several people could begin to.

First, after, later, never

When should we tidy? Beck’s answer is a question with four possible answers: first, before the behavior change; after it; later, as separate work; or never. Tidy first when the tidying makes the change cheaper and we are about to make it. Tidy after when the change has taught us what the structure should have been. Tidy later when the tidying is worthwhile but unrelated to today’s work. And never, when the code will not change again, because structure that nobody will read pays nothing.

Beck grounds this in finance. A euro today is worth more than a euro tomorrow, so behavior delivered sooner has more value, which argues against tidying. But structure creates options, the ability to make future changes cheaply, and options have value even before we know which changes we will want, which argues for it. Beck then states what he calls Constantine’s equivalence: the cost of software is approximately the cost of changing it, which is approximately the cost of its big changes, which is approximately its coupling. The equivalence names Larry Constantine because coupling and cohesion as design ideas come from him, first set out with Wayne Stevens and Glenford Myers in the IBM Systems Journal in 1974 and developed in Yourdon and Constantine’s Structured Design. The Architecture book’s Section 1.3 teaches the concepts; Beck’s contribution is to connect them to money.

The connection gives engineers a vocabulary that product managers and finance already speak. “This module is ugly” persuades nobody outside the team; “every change here costs us an extra week, and we have 11 changes planned this year” can be weighed against a feature. Beck offers this as an argument from finance, without measurements behind it, and Encore uses it that way: as a frame for a decision, with the numbers taken from the team’s own history.

Smells as prompts

Fowler and Beck catalog code smells, patterns that suggest a refactoring might help: shotgun surgery, where one change requires many small edits in many places; divergent change, where one module changes for many unrelated reasons; feature envy, where a function is more interested in another module’s data than its own. They present smells as prompts for judgment, and they warn that no set of metrics rivals informed human intuition in deciding when to act.

The warning has empirical support. Dag Sjøberg and colleagues had six developers carry out the same maintenance tasks on four functionally equivalent Java systems, 298 files in all, and measured the effort each file required. Once they controlled for file size and for the number of changes made, none of the 12 smells they studied was significantly associated with more effort. Size and change count explained nearly all of it. The result should sober anyone who has proposed a smell count as a quality goal, and it points to the next section: if effort follows size and change, then change history is where to look.

Recap. Refactoring changes structure in small, named, checkable steps without changing behavior, and tidyings are its smallest unit, done just before the change that needs them. Structure and behavior belong in separate changes, because mixing them blinds the reviewer and corrupts the history every later analysis depends on. Beck’s economics give us a language for when to tidy, and smells are prompts for judgment, since counting them predicts little.

Exercise 2.2. Take your last behavior change and split it, after the fact, into the tidying and the change. How many lines would each pull request have had, and which of the two would a reviewer have needed to read closely?

2.3 Technical Debt as Data

The metaphor and its drift

In 2009, 17 years after the WyCash report, Cunningham recorded a short video explaining the debt metaphor, because, as he put it, “A lot of bloggers at least have explained the debt metaphor and confused it, I think, with the idea that you could write code poorly with the intention of doing a good job later.” “I coined the debt metaphor to explain the refactoring that we were doing on the WyCash product,” he said; if the program failed to align with what the team now understood about financial objects, they would keep stumbling over the disagreement, “which was like paying interest on a loan.” Then he drew the line that bloggers had blurred: “I’m never in favor of writing code poorly, but I am in favor of writing code to reflect your current understanding of a problem even if that understanding is partial.” The 2009 account is a faithful reading of the 1992 report, which spoke of consolidation through rewriting, but we should keep the dates apart: the coinage is from 1992, and the gloss about refactoring from 2009.

Cunningham mentioned in the same video that his interest in how metaphors shape thought came from George Lakoff and Mark Johnson’s Metaphors We Live By (1980), and that is a good lens for what happened next. A metaphor imports a whole structure of thought. “Debt” brought principal, interest and repayment, which served his purpose, and also the idea that debt is a choice one makes to buy something now, which the industry ran with until “technical debt” meant any shortcut, any old code, any design the speaker disliked. Martin Fowler tried in 2009 to sort the meanings with a quadrant, crossing reckless with prudent and deliberate with inadvertent, so that “we don’t have time for design” and “now we know how we should have done it” could be told apart. In 2016 a seminar of researchers at Dagstuhl, convened by Paris Avgeriou, Philippe Kruchten, Ipek Ozkaya and Carolyn Seaman, agreed on a definition centered on design or implementation constructs that are expedient in the short term but make future change more costly or impossible. Figure 2.2 lays the history out in order.

The debt metaphor, 1980 to 2016 A timeline from 1978 to 2018 on a proportional scale. Above the line: Lakoff and Johnson's Metaphors We Live By (1980), the book Cunningham later said shaped his interest in metaphor; Ward Cunningham's WyCash experience report, dated 26 March 1992 and presented at OOPSLA in Vancouver in October 1992, which coins the debt metaphor and says the debt is paid back with a rewrite; Martin Fowler's Refactoring (1999), which made refactoring a working vocabulary; and Cunningham's 2009 video, which retells the repayment as refactoring and says the metaphor was never a license to write code poorly. Below the line: William Opdyke's Illinois doctoral thesis (1992), which first defined refactoring in depth, in the same year as the report; Fowler's technical debt quadrant (2009), crossing reckless with prudent and deliberate with inadvertent; and the Dagstuhl seminar of April 2016, which agreed a definition centred on constructs expedient in the short term that make future change more costly or impossible. A bracket marks the 17 years between the coinage and Cunningham's gloss. The two Cunningham entries are highlighted. 17 YEARS: KEEP THE DATES APART 1980 · LAKOFF & JOHNSON Metaphors We Live By metaphors shape thought MARCH 1992 · CUNNINGHAM The WyCash report debt paid with a rewrite 1999 · FOWLER Refactoring a working vocabulary 2009 · CUNNINGHAM The debt video repaid by refactoring 1992 · OPDYKE, ILLINOIS Doctoral thesis refactoring defined 2009 · FOWLER The debt quadrant reckless vs prudent APRIL 2016 Dagstuhl 16162 agreed definition HOW ONE METAPHOR WAS COINED, RETOLD AND REDEFINED · PROPORTIONAL TIME × DELIBERATE VS INADVERTENT 1980 1990 2000 2010 Cunningham's own words Work on the idea by others

Figure 2.2 — The debt metaphor from its coinage to its consensus definition. Cunningham’s two statements stand 17 years apart, and the word for repayment changed between them, from “a rewrite” to “refactoring”.

In this book, technical debt is the gap between the structure of the code and the team’s current understanding of the problem, measured by what it costs each change that must cross it. That definition is closer to Cunningham’s than to common usage, and it has one practical virtue: it makes debt something we can find in data. “Our code is full of debt” ends an argument, because there is nothing to say in reply. “Changes to this file take four times longer than changes elsewhere, and we plan 11 of them this year” starts a decision. The rest of this section is about measuring such debt, and the measurements begin in version control.

Hotspots

In the late summer of 1854, during a cholera outbreak in Soho, the physician John Snow marked the deaths on a map of the streets and saw them cluster around the public water pump on Broad Street. He persuaded the parish authorities to remove the pump’s handle. What he left us is a method: before deciding what to treat, find where the cases are. Adam Tornhill’s Your Code as a Crime Scene applies the same habit of mind to code. A hotspot is code that is both complex and frequently changed, found by combining a complexity measure for each file with its change frequency mined from version control. Complex code that nobody touches costs little; simple code that changes daily costs little; code that is both complex and constantly changed is where effort and defects concentrate.

The evidence behind this is older and stronger than the vendor literature. Todd Graves and colleagues, studying the change history of a large, long-lived system in 2000, found that the number of past changes to a module predicted its future faults better than its size did. Thomas Ostrand, Elaine Weyuker and Robert Bell, predicting faults across 15 releases of large industrial systems, found that the 20% of files their model ranked highest contained about 83% of the faults on average. Nagappan and Ball showed in 2005 that churn measured relative to size predicted defect density in Windows. The common thread is that a small fraction of files attracts most change and most defects, and that version control already knows which files they are.

Snow’s map breaks as an analogy at one point. Every mark on his map was a death, while most commits are healthy work, and a file that changes often may simply be where the business is moving fastest. A hotspot tells us where to look, and the looking is still ours to do. Tornhill’s advice is to read them as trends over time, asking whether a hotspot’s complexity is growing or shrinking, and to confirm each one with the people who work in it.

Encore’s Developer Portal team added a hotspot report to the service registry the year before, and the Inventory and Resale teams ran it across the purchase-path repositories for the previous twelve months. Figure 2.3 shows the top five, set by size against the number of changes.

Hotspots on the purchase path A scatter chart of Encore's top five hotspots on the purchase path over twelve months, with size in lines across and number of changes up. 1: ticketing/barcode/BarcodeIssuer.kt, 214 changes, 3,900 lines, 88% of changes by its top author. 2: orders/saga/PurchaseSaga.kt, 187 changes, 2,600 lines, 21%. 3: ticketing/barcode/BarcodeFormats.kt, 131 changes, 1,450 lines, 91%. 4: inventory/holds/HoldService.kt, 96 changes, 1,700 lines, 34%. 5: orders/psp/PspRouter.kt, 88 changes, 1,100 lines, 29%. The two barcode files are highlighted: nearly all their changes come from one person, while the saga's are spread across many authors. 0 1,000 2,000 3,000 4,000 0 50 100 150 200 250 SIZE (LINES) CHANGES IN 12 MONTHS 1 · ticketing/barcode/BarcodeIssuer.kt 214 CHANGES · 3,900 LINES · TOP AUTHOR 88% 2 · orders/saga/PurchaseSaga.kt 187 CHANGES · 2,600 LINES · TOP AUTHOR 21% 3 · ticketing/barcode/BarcodeFormats.kt 131 CHANGES · 1,450 LINES · TOP AUTHOR 91% 4 · inventory/holds/HoldService.kt 96 CHANGES · 1,700 LINES · TOP AUTHOR 34% 5 · orders/psp/PspRouter.kt 88 CHANGES · 1,100 LINES · TOP AUTHOR 29% Barcode issuance: 88% and 91% of changes by one person Other purchase-path file

Figure 2.3 — Encore’s hotspot run on the purchase path, one year of history. Two of the top three files belong to barcode issuance, and nearly all their changes come from one person.

That barcode issuance and the purchase saga’s orchestrator sat at the top surprised nobody; they are where every new ticket type, payment rule and channel must land. The share of changes by each file’s top author surprised everyone. The saga’s changes were spread across many authors, while the barcode files’ changes came almost entirely from one engineer, because every team that needed something from issuance had learned over the years to ask that person to make the change.

Change coupling

Static analysis sees the dependencies written in the code: this file imports that one, this function calls that one. It cannot see the dependencies that live only in people’s heads. Change coupling is the tendency of two files to change together over time even though nothing in the code links them, and it reveals exactly those hidden dependencies. Harald Gall, Karin Hajek and Mehdi Jazayeri introduced the idea in 1998, as logical coupling found in the release history of a telecommunications switching system, and Thomas Zimmermann and colleagues showed in 2004 that mining version histories this way could suggest to developers which other files a change would probably need.

The method has conditions, and they follow from Section 2.2. It assumes commits are cohesive, each containing one logical change. Tangled commits, large automated changes of the kind Section 2.5 describes, and formatting sweeps all create false coupling, because they touch many files for reasons unrelated to one another. A sound analysis filters out commits above a size threshold and commits made by bots. Across services, files rarely change in the same commit, so coupling has to be found through the work item that links the commits, or a time window around them.

At Encore the analysis was run by work item across repositories, and it found that 70% of the changes to BarcodeFormats.kt were accompanied, in the same work item, by a change to Venue Entry’s offline validator, the code in the door scanners that checks a barcode without a network. That coupling is legitimate, since the scanners must understand every format the issuer produces, but nobody had written it down, and twice that year a format change had reached the issuer before the validator. It also found a stranger coupling, with the Support Tools agent console, to which Section 2.5 returns. Findings like these feed the decomposition work of the Architecture book’s Sections 3.3 and 3.4; here they showed that the barcode format was a contract, though nobody had declared it one.

Knowledge maps and the truck factor

That top-author share in Figure 2.3 is a knowledge map in miniature. A knowledge map shows, for each part of a codebase, who has written and changed it, and it is how the truck factor that Section 1.3 defined is estimated in practice. Guilherme Avelino and colleagues, whose finding that 65% of 133 popular open-source systems had a truck factor of two or less Section 1.3 cited, computed their estimates from exactly this kind of authorship data.

Ownership affects quality as well as risk. Christian Bird and colleagues found in a 2011 study of Windows that components with many minor contributors, people who made only a small share of the changes, had more defects. In 2008 Nagappan, Brendan Murphy and Victor Basili found that measures of organizational structure, such as how many engineers and how many parts of the organization touched a binary, predicted the failure-proneness of Windows Vista binaries better than churn, complexity, coverage or dependencies did. It is as close as we have to Conway’s law made measurable. These results need caution, since a later study by Greiler, Herzig and Czerwonka found the link between ownership and quality in some projects and not in others, but their general shape is that both extremes cost something: ownership concentrated in one person risks continuity, and ownership scattered across many occasional contributors risks quality.

Two cautions govern these maps. Truck-factor algorithms measure authorship, only a proxy for understanding; someone who reviewed every change to a module may know it well and appear nowhere. And a knowledge map shows where the organization is fragile, so it must never be used to rank the people in it. Encore’s rule for scorecards applies here too: teams see their own maps, leadership sees trends, and nobody sees a league table.

Barcode issuance had a truck factor of one, the 1992 report’s “extreme specialization of programmers” in its purest form. The remedies are old and well practised. Pairing puts a second person beside the expert for real changes, so that knowledge transfers where it is used. Ensemble sessions, a whole team working on one problem at one screen, spread it faster at a higher price in hours. Rotating reviews through the module turns routine work into teaching. And a written guide to the module, its history, its quirks and the reasons behind them, keeps what the expert knows available when the expert is on holiday. From month five the Inventory team paired two engineers with Lena on every barcode change, and one of them began the guide. Raising a truck factor takes quarters, and Chapter 11 tells how far this one rose.

The business case and the debt register

The case for repaying debt has to be made in currencies the business already counts: time, predictability and defects. Adam Tornhill and Markus Borg’s “Code Red” study (2022) is the most direct evidence. Across 39 proprietary codebases and 30,737 files, code with low health scores had 15 times as many defects as healthy code, took on average 124% longer to resolve issues in, and had maximum cycle times nine times longer. The study is correlational, and its quality measure is the proprietary Code Health metric of CodeScene, where both authors work. Terese Besker, Antonio Martini and Jan Bosch found in 2019, in a longitudinal study of 43 developers, that they reported wasting about 23% of their working time because of technical debt; the figure is self-reported and says most about how much developers feel the drag.

Encore’s own numbers were more persuasive than either study, because they were Encore’s. Over the year, the median change touching barcode issuance took 11 days from first commit to production, against two days for the rest of Seat Inventory & Ticketing, and the gap came largely from the wait for the one person who could review it. Verified Resale’s plan called for at least 14 changes to that code before launch. That arithmetic went into the quarterly plan as a request for capacity, which Section 10.4 describes, and it was granted without much argument.

What the arithmetic needed was a place to live after the plan was approved. Philippe Kruchten, Robert Nord and Ipek Ozkaya’s Managing Technical Debt (2019) recommends recording debt items explicitly, and the practice has a simple form. A debt register is a list of known debt items, each with its location, the interest it charges, an owner, and a trigger that forces repayment. The trigger is what keeps the register honest: an item without one is a complaint, and an item with one is a commitment that something will happen when a stated condition arrives. Encore’s register lives beside each service’s entry in the service registry, and its first barcode entry read:

- id: INV-DEBT-017
  where: ticketing/barcode/BarcodeIssuer.kt (issue* variants)
  what: 19 public issue variants; callers choose by channel and era
  interest: median change 11 days vs 2 elsewhere; one reviewer
  owner: Inventory team
  trigger: before the first production reissue, or before any new channel
  repay: collapse to one issue() with a typed request, behind the reissue seam
  decided: month 5, Year 9 (quarterly plan)

The entry answers, once and in writing, the questions the team would otherwise have answered differently every time someone asked them.

Recap. Cunningham’s debt was the gap between code and current understanding, repaid by consolidation; its drift into “anything we dislike” made the word end arguments it should start. Version control holds the data to measure it, through hotspots, change coupling and knowledge maps, and each item of debt is written down with its interest, an owner and a trigger.

Exercise 2.3. Run a hotspot analysis on one repository for the last year. Before looking at the result, ask the team to name the five files they expect at the top. Compare the two lists, and for each surprise, write one debt-register entry with interest, owner and trigger.

2.4 Legacy and Modernization

Legacy is code without tests

The word “legacy” usually carries a sneer, and the sneer gets the problem wrong. Michael Feathers, in Working Effectively with Legacy Code (2004), offers a definition that moves attention from the age of the code to the thing that actually makes it frightening: “To me, legacy code is simply code without tests.” The definition earns its place because it names the remedy in the same breath as the condition. Legacy code is code we cannot change safely because nothing will tell us when we have broken it. Code written last month without tests is legacy in this sense, and eight-year-old code with good tests is not.

Marianne Bellotti, who modernized government systems with the United States Digital Service, makes a complementary point in Kill It with Fire (2021): old systems survive because they were successful, well fitted to the constraints of their time, and modernizing them means respecting what they got right as well as fixing what has aged. The Architecture book’s Section 3.4 made the same argument. Encore’s barcode issuer had issued every ticket the company ever sold, through two changes of scanner vendor, the year-four split into services and the year-eight move to rotating barcodes. Whatever was wrong with it, it had been right about a great many things, and the first task was to find out exactly which.

The legacy change algorithm

Feathers gives a procedure for changing code without tests, and its value lies in its order: identify the change points; find the test points, where tests can observe the change’s effects; break the dependencies that keep the code out of a test harness; write the tests; and only then make the change and refactor. The order matters because the instinct of every engineer facing frightening code is to begin at step five, carefully, and the procedure exists to stop us. Feathers adds two tools for choosing test points: effect sketches, which trace what a change can affect, and pinch points, narrow places in the code through which many effects pass, where a few tests can cover a great deal.

Figure 2.4 shows the procedure as the Inventory and Resale teams applied it between months four and nine.

The legacy change algorithm Five steps in order, left to right, as the Inventory and Resale teams applied them to the barcode issuer between months four and nine. 1, identify the change points: issuing a ticket to a new holder, revoking the old identity, and recording both in TicketIssued events. 2, find the test points: one pinch point, the method every issue variant finally calls to build and persist a ticket. 3, break dependencies: the issuer read the clock, the secret store and the event-code table directly, and each became a constructor parameter. 4, write tests: a characterization suite of 1,400 cases, most generated by replaying a year of issuance requests. 5, only then change and refactor: Reissue sprouted beside the issuer, and the issue variants collapse behind it later, as debt item INV-DEBT-017. Steps 3 and 4, highlighted, took most of the effort; the change itself was small. A note marks step 5 as where the instinct says to begin. MOST OF THE EFFORT A SMALL CHANGE 1 · IDENTIFY Change points 2 · FIND Test points 3 · BREAK Dependencies 4 · WRITE Tests 5 · ONLY THEN Change, refactor BARCODE ISSUER · MONTHS 4–9 · READ LEFT TO RIGHT issue to a new holder; revoke the old identity; record both in TicketIssued events one pinch point: the method every issue variant finally calls to build and persist clock, secret store and event-code table, read directly, each became a constructor parameter characterization suite of 1,400 cases, most from a replayed year of issuance requests Reissue sprouted beside the issuer; the issue variants collapse behind it later (INV-DEBT-017) THE INSTINCT: START HERE Where the effort went Step In this order

Figure 2.4 — Feathers’s legacy change algorithm applied to Encore’s barcode issuer. Most of the effort went into the third and fourth steps, breaking dependencies and writing tests; the change itself was small.

Seams and characterization tests

Breaking dependencies needs somewhere to break them. Feathers calls such a place a seam. A seam is a place where we can alter a program’s behavior without editing the code at that place. An object seam lets a test substitute a different object through an interface or a constructor parameter; a link seam substitutes a different library at build time; a preprocessing seam, in languages that have a preprocessor, substitutes at compile time. The barcode issuer needed object seams it did not yet have. It called the system clock directly, so no test could ask what happened when an event was rescheduled, and it fetched rotation secrets from the secret store directly, so no test could run without production credentials. Making each a constructor parameter, the technique Feathers calls Parameterize Constructor, changed no behavior and made every later test possible.

With seams in place, the tests that come first are of a particular kind. A characterization test records what the code actually does, so that we notice if a change alters it, whether or not that behavior is what anyone intended. It is a description of the present, with no claim about what is correct. Before renovating an old building, architects commission a measured survey of what is actually there, because the original drawings describe what someone planned, and decades of alterations lie between the plan and the walls. Characterization tests are that survey for code. The analogy breaks usefully: a building stands still while it is measured, and a codebase changes every day, so the survey must keep running in continuous integration for as long as anyone alters the code.

The practical tool for characterizing at scale is approval testing, sometimes called golden-master testing: feed the code a large set of inputs, record its outputs, and on every later run compare the new outputs with the recorded ones. The Inventory team replayed a year of issuance requests, with personal data removed, through the issuer in a test harness, and recorded every barcode, check digit and event. Eight years of quirks came to light. Festival passes carried the festival’s code in place of any single day’s event code. Tickets issued before the year-four split still carried an older check-digit scheme that the scanners accepted. A ticket for a rescheduled event kept its original event code forever. Nobody had complained about any of these, and a careless reissue would have broken every one. Each became a named test, written so that a reader could see it described the past without endorsing it:

// Characterization: pins what the issuer does today. Not a specification.
@Test fun `festival pass carries the festival code, not a day's event code`() {
    val pass = issuer.issue(sale(product = FESTIVAL_PASS, festival = "F-0412",
                                 days = listOf("E-20431", "E-20432")))
    assertTrue(pass.barcode.startsWith("0412"))
}

@Test fun `rescheduled event keeps its original event code`() {
    val ticket = issuer.issue(sale(event = "E-18807", rescheduledTo = "E-21950"))
    assertTrue(ticket.barcode.startsWith("18807"))
}

Chapter 3 asks what a test is worth and how a suite grows from tests like these into one that specifies behavior; here they serve as a net under the old code while the new code is built beside it.

Sprout and wrap

With the old code characterized, the temptation is to edit it, and Feathers offers a better default: write new behavior as new, tested code, and call it from the old code with as small a change as possible. Sprout Method and Sprout Class put new behavior in a new method or class, called from one line in the old code. Wrap Method and Wrap Class put it around an existing method or class, adding something before or after the old behavior. In each case the new code is tested from birth, and the old code changes by a line or two that the characterization tests can vouch for.

Reissue was sprouted as a class, the interface of Section 2.1, living beside the issuer inside Seat Inventory & Ticketing and owned by the Inventory team. Whether Resale or Inventory should own it had been argued in a request for comments (RFC) and settled in an architecture decision record (ADR), as Section 9.5 describes: Inventory owns issuance and revocation, and Resale calls it. Inside, Reissue made exactly one call into the old issuer, through the pinch point the tests already covered, and did everything new, the revocation, the new opaque identity and the event for the ledger, in tested code of its own. Figure 2.5 shows the arrangement.

Reissue, sprouted beside the issuer Inside Seat Inventory & Ticketing, owned by the Inventory team, two pieces of code sit side by side. On the right is the old barcode issuer: existing callers choose one of its issue variants, and every variant finally calls one pinch point that builds and persists a ticket. A characterization suite of 1,400 cases, running in continuous integration, pins what that pinch point does today. On the left is Reissue, a new class tested from birth. The Resale service calls it with a reissue request; Reissue makes exactly one call into the old issuer, through the pinch point the tests already cover, and does everything new in its own code: it revokes the old identity, issues a new opaque ticket_id, and emits the event for the ledger. Inventory owns issuance and revocation; Resale calls it, as an architecture decision record settled. SEAT INVENTORY & TICKETING · INVENTORY TEAM THE OLD BARCODE ISSUER CHOOSE A VARIANT reissue(request) ONE CALL PINS TODAY'S BEHAVIOR Existing callers today's issuance Resale service Resale team SPROUTED Reissue new class · own tests issue* variants callers choose by channel, era Pinch point builds and persists a ticket Characterization suite 1,400 cases · in CI Everything new, in tested code: · revokes the old identity · issues a new opaque ticket_id · emits the event for the ledger New, tested code Old code Tests Caller The one call into the old code

Figure 2.5 — Reissue sprouted beside the old issuer. The new code is tested from birth, and its single call into the old code passes through the pinch point the characterization suite already covers.

One line separates these techniques from a larger relative. Sprout and wrap work inside a class or a module. When a whole component must be replaced while the codebase keeps running, the pattern is branch by abstraction, which the Architecture book’s Section 3.4 teaches at the scale of a system and Section 4.5 of this book applies within a codebase behind flags. The barcode format change that follows Reissue is of that larger kind.

A modernization without a rewrite

Every team that inherits old code feels the pull of the rewrite, and the Architecture book’s Section 3.4 has already named the pull: the second-system effect, and the strangler fig as the pattern that replaces a system piece by piece instead. What remains is the running of the program, where modernizations most often fail, and Bellotti’s book is a good guide to it, drawn from experience. The claim that rewrites usually fail is itself mostly anecdote, the best known being Joel Spolsky’s 2000 essay on Netscape’s decision to rewrite its browser from scratch, and we should not dress it up as a statistic.

Bellotti’s central observation is that momentum is the scarce resource. A modernization is a long effort whose benefits arrive late, and it dies more often because people stop believing it will finish than because it hits a technical wall. So the program has to produce visible wins early, and it has to define “done” before it begins, since an effort without that definition tends to be abandoned at 80%, leaving both the old system and the new one to maintain. Decommissioning belongs in the scope from the start, because a modernization that adds a new path without removing the old one has increased the system’s complexity. And a team joining a modernization midway should diagnose before it acts: what has been done, what has been promised, and what the old system still does that nobody has written down.

Encore’s plan for the barcode core followed these lines. It did not rewrite the issuer; it changed it in place behind seams, with the strangler pattern held in reserve should the in-place work stall. It defined done in one sentence: every ticket issued or reissued through one typed interface, the issue variants deleted, and a truck factor of at least three. It put the deletion of the old variants in the plan as debt item INV-DEBT-017 with a trigger. And it chose its first visible win carefully: the first characterization suite running in continuous integration, which the whole Inventory team could see, by month six. Chapter 11 tells how the plan held.

Recap. Legacy code is code without tests, and the remedy follows from the definition. Feathers’s procedure puts the tests before the change: find the change points and a pinch point, break dependencies through seams, characterize what the code does today, then sprout or wrap new behavior in new, tested code. A modernization without a rewrite is run as a program, with momentum protected, done defined in advance and decommissioning in scope.

Exercise 2.4. Find the least-tested file among your top hotspots. Without changing a line of it, write three characterization tests that pin behavior you did not know it had. Then name the seam you would need to make the fourth test possible.

2.5 Changing Code at Scale

Programming over time

Everything so far could be practised by one team on one module. Scale adds a dimension that changes the economics of every practice: time multiplied by people. Titus Winters, in the opening chapter of Software Engineering at Google, put the distinction in one line: “Software engineering is programming integrated over time.” A program written for a weekend need not survive a new compiler, a new library version or its author’s departure; code meant to live for a decade must survive all of them, many times.

From that framing follows a policy test: “everything your organization must do repeatedly should be scalable in terms of human effort.” A practice whose cost grows linearly with the codebase or the headcount will eventually consume the organization. At nine engineers, asking the author of the barcode code to review every change to it cost a few minutes a week. At 400 it became the 11-day wait of Section 2.3, because the changes grew with the company and the reviewers stayed at one. The practices in the rest of this section are those whose costs grow slowly.

Hyrum’s law inside the walls

The Architecture book’s Section 7.2 stated Hyrum’s law for public interfaces: with a sufficient number of users of an API, every observable behavior of the system will be depended on by somebody, whatever the contract promises. The law holds just as firmly inside a company, where it is easier to forget, because internal callers feel like colleagues.

The history of the Go language gives a clean public example. The Go 1 release in March 2012 made the iteration order of maps deliberately unpredictable, precisely so that programs could not come to depend on it. A new map implementation in Go 1.1 neglected to randomize iteration over maps of eight or fewer entries, so in Go 1.1 and 1.2 those small maps iterated in an order that was consistent on a given system. Programs came to depend on it, and when Go 1.3 reintroduced randomization for small maps in 2014, its release notes warned that code assuming a fixed order would break, adding that the problem would arise most often in tests. The designers had anticipated the dependency, and it formed anyway in the gap their own implementation left open. An older case is told by Joel Spolsky in his 2004 essay “How Microsoft Lost the API War”: the game SimCity read memory after freeing it, and rather than break the game, Windows added a special mode for its memory allocator that it switched on when SimCity was running.

Encore’s ticket identity had collected its own dependents. The legacy barcode, and the identity it encodes, begins with the event code, an accident of how the year-one issuer derived identity from order and seat, never documented as a promise. Rowmate’s integration with Partner Listings, written in year six, read that prefix to match a ticket a seller had forwarded to the right event page. The dependency survived the move to rotating barcodes in year eight, because rotation was built around the existing identity without changing it, and so for three years nobody noticed it. It surfaced only when Verified Resale replaced the identity itself. Partners had been told at month 2 that reissued tickets would carry a new, opaque identity, and the old format stays accepted on the partner API until month 14, which keeps Encore’s promise of 12 months’ notice. Even so, Rowmate’s matching code failed against the sandbox when Rowmate began certification in month 9, and Rowmate’s engineers rewrote it during certification, before any fan depended on it. Inside Encore, the change-coupling analysis of Section 2.3 found the same dependency in the Support Tools agent console, which derived a ticket’s event from the first digits of its barcode so that agents could find a fan’s booking quickly. Neither dependent had asked permission, and nothing had told either that it could not: the prefix was observable, and that was enough.

The lesson Encore drew is built into the new design. The ticket identity that Reissue issues is opaque: a ticket_id that carries no meaning anyone can parse. Partners receive it through the API and never parse barcodes, which rotate in any case, and internal callers that need a ticket’s event ask Seat Inventory & Ticketing for it. An identifier with nothing observable inside it gives Hyrum’s law nothing to work with. A decade of maintaining an open-source library has convinced me that documentation does not prevent such dependencies; only removing the observable behavior does, or, where it must stay observable, committing to it.

That commitment has a name inside Google, recorded in the same book. The Beyoncé Rule reads: “If you liked it, you should have put a CI test on it.” It is the internal remedy for Hyrum’s law, and it cuts in both directions. A team that depends on another team’s behavior should protect that dependency with a test in the provider’s continuous integration (CI), so that the provider learns of the dependency before breaking it. And a provider making a change that breaks no test is entitled to make it. At Encore, Venue Entry’s offline validator now contributes tests to the barcode code’s suite, so that the coupling Section 2.3 found is declared where the issuer’s owners will see it.

Large-scale changes

Some changes cannot be made in one commit. Hyrum Wright’s chapter in Software Engineering at Google defines a large-scale change (LSC) as “any set of changes that are logically related but cannot practically be submitted as a single atomic unit”, whether because they touch too many files for any build to test at once, or because they cross too many owners for any single review. Google generates such changes with compiler-aware tools, such as ClangMR for C++, which Wright and colleagues described in 2013, and Refaster for Java. A system called Rosie splits each change into shards along project and ownership lines, tests each shard independently, routes it to the owners or to a global approver, and submits it. The migration away from scoped_ptr touched more than 500,000 references and at its peak landed more than 700 changes a day touching more than 15,000 files. Google reports that changes of this kind account for 10% to 20% of the changes in a typical project.

The process has four steps: authorization, in which the change is approved as worth making across the codebase; change creation, by tools; sharding and submitting; and cleanup, which removes the old way once the new one has spread. Aviation has run fleet-wide changes this way for decades. When an authority such as the U.S. Federal Aviation Administration issues an airworthiness directive, every affected aircraft must be inspected or modified by a deadline, aircraft by aircraft, each signed off separately: one decision, many small executions, each verified on its own. The parallel breaks at the source of authority. A directive is law, while a large-scale change inside a company rests only on the authority the organization grants its global approvers, a grant that must be made explicitly.

The method has conditions. It needs tools that understand the language’s types, so that generated changes are correct by construction; a CI system that can test each shard cheaply; and an ownership policy that lets a global approver land a mechanical change in a team’s code without that team’s line-by-line review. Without those, the honest alternative is a campaign: a tracked migration in which each team makes the change in its own code, supported by a codemod and a dashboard. Stripe’s account of its move from Flow to TypeScript shows a third shape: its engineers built a codemod, later open-sourced, and on 6 March 2022 converted more than 3.7 million lines of the Dashboard codebase in a single pull request.

Encore’s barcode format change, which moves every internal caller from parsing barcodes to asking for the opaque ticket_id, will be a change of this kind, across 11 services, and Chapter 11 follows it. Machine assistance now writes many such changes, and Chapter 5 examines where it helps and how its work is checked; the mechanics described here, of sharding, testing, approving and landing, are the same whoever writes the shards.

Deprecation as a process

Large-scale changes move callers to the new way. Deprecation removes the old one, and it is the half of the work organizations most often neglect, because removing code earns little praise. Wright’s chapter on deprecation treats code as a liability to be justified by what it provides, so that two ways of doing one thing cost maintenance, security patches and the confusion of every new engineer who finds both.

The chapter distinguishes two kinds. Advisory deprecation marks the old way as discouraged and hopes callers will move; Google’s verdict on it is “Hope is not a strategy,” because callers with working code and other priorities rarely move on their own. Compulsory deprecation has a deadline and enforcement, and it works only when someone owns it, with time allotted, because the last few callers are always the hardest. Figure 2.6 sets the code-level mechanics side by side.

Mechanism Advisory Compulsory
Discovery of callers Code search, when someone looks Code search plus runtime logging, kept current by the owner
Warnings On every use, everywhere Only on newly changed lines, so they are acted on
New uses Allowed Blocked by a visibility allowlist or a build rule
Migration Each team, when it chooses Tooling or an LSC, with a date
Owner Often nobody A named team, staffed for the work

Figure 2.6 — Advisory and compulsory deprecation at the level of code. Encore uses the right-hand column for anything it intends to delete.

Two details in the figure carry more weight than they seem to. Google shows deprecation warnings only on lines a developer is already changing, because warnings on every old use teach people to ignore all warnings. And it prevents backsliding with visibility allowlists in its build system, which list exactly which code may still depend on the deprecated target, so that the list can only shrink. Encore gets the same effect from an ArchUnit rule that fails the build for any new import of a deprecated package outside a named allowlist. When Encore deprecated the legacy barcode parser in month eight, the Inventory team owned the deprecation, the allowlist named the four remaining callers, and the build turned away a fifth in its first week. Platform-wide deprecation campaigns, with brownouts and partner timelines, are Chapter 6’s subject.

Dependencies and one version

The last practice concerns the code we did not write. Software Engineering at Google, in its chapter on version control, states what it calls the One-Version Rule: “For every dependency in our repository, there must be only one version of that dependency to choose.” Violations, it says, “lead to merge strategy discussions, diamond dependencies, lost work, and wasted effort.” In a diamond dependency, two libraries a service uses depend on different versions of a third, and the service cannot satisfy both. Google is candid that the rule “is not hard and fast”, and that what matters is adhering to the one-version principle as far as possible, monorepo or not. Its chapter on dependency management adds that its policy of at least two owners for every third-party package has failed when the owners moved on.

A company with many repositories, like Encore, can approach the rule without a monorepo. A shared version catalog or bill of materials (BOM), included in every service’s build through the golden-path template, names one version of each third-party library, and automated update tools such as Renovate or Dependabot open pull requests to move each service to the catalog’s version, gated by that service’s own tests. Section 4.2 weighs the monorepo against the polyrepo; the one-version discipline is available under either.

Who does the upgrading is a policy choice, and Google’s “Live at Head” places the burden on whoever changes the shared version. Encore made the same choice explicitly: the team that owns a library’s entry in the catalog upgrades every caller, with the update tools doing the mechanical part. Its worked case was the date-time library on which every service that computes a schedule depends, whose catalog entry belongs to the Event Setup team. A survey in month six found four versions in use across Encore’s services, two of them bundling different releases of the time-zone database. That mattered for Verified Resale in a specific way: the default resale window closes two hours before doors, and the Resale service and the gate apps must agree on when that is, in every time zone where Encore sells. Event Setup moved all services to one version in three weeks, most of it through automated pull requests, and added a build check that fails any service declaring its own version of the library.

Recap. At scale, the test of any practice is whether its cost grows more slowly than the organization. Hyrum’s law applies inside the company, and its remedies are to remove observable accidents, such as a meaningful prefix in an identifier, and to protect deliberate dependencies with tests in the provider’s CI. Large-scale changes land as many small, tested shards; deprecation finishes only with an owner, a date and enforcement; and each dependency has one version, upgraded by its owner.

Exercise 2.5. For one internal library your team provides, list the observable behaviors callers might depend on that no test protects: orderings, formats, timings, error messages, identifiers with structure inside them. For each, decide whether to remove it, or to commit to it with a test.

Kata: The Rostering Rules

Your brief: “Ledgermoor.” Ledgermoor is a 12-year-old crew-scheduling SaaS used by 31 airlines in eight EU countries, from regional carriers to two mid-size flag carriers, building the monthly rosters of about 60,000 pilots and cabin crew. It has 120 engineers in 14 teams. At its heart sits the rostering-rules engine, roughly 60,000 lines written in the company’s first three years, which checks every pairing and roster against the EU’s flight-time limitations, national variations and each airline’s labor agreement; its two principal authors left in the last 18 months, and one engineer who worked beside them is consulted on nearly every change. Test coverage averages about 55% across the codebase and 6% in the engine. Labor agreements are renegotiated every year or two, and a change to the engine takes a median of 19 days from first commit to production, against four elsewhere. Twice last year a rule change produced rosters that broke a crew member’s minimum rest, caught by airlines’ crew planners before publication. Leadership has asked for a rewrite; the head of engineering has asked you instead for a plan. You have one quarter and one team of seven, and two airlines’ new labor agreements must be enforced by the engine before the winter schedule begins.

Deliverables:

  1. Hotspot and knowledge map — Describe the analysis of the engine’s history: the window, the complexity measure, how you will filter tangled and automated commits, and how you will show where knowledge has concentrated. Predict what it will find.
  2. Debt register — Write the first five entries, each with location, interest in days or defects, owner and trigger. At least one must be debt the team chooses not to repay this quarter.
  3. Characterization strategy — Explain how you will pin what the engine does today: the pinch points, the seams you must create, and how last year’s published rosters, with crew identities removed, become approval-test inputs. Say who decides whether a quirk is preserved or fixed.
  4. The first change — Show how one of the new labor-agreement rules enters the engine as sprouted or wrapped code, with the line it changes in the old code and the tests that vouch for it.
  5. Knowledge plan — Show how the engine’s truck factor will rise this quarter, and how you will know it has, without ranking anyone.
  6. The case against the rewrite — Write the one-page argument for leadership in days, predictability and defects, with the quarter’s definition of done and the conditions under which a strangler-fig replacement would become right.

Rubric:

Criterion The question to ask yourself
Evidence before opinion Does every claim about the engine rest on history, tests or measured cycle times, and not on its reputation?
Tests before change Is the first rule change made only after the code it touches is characterized?
Honest debt Does the register include interest, owners and triggers, and does it admit what will not be fixed?
Knowledge as risk Does the plan raise the truck factor without turning the knowledge map into a ranking of people?
Momentum Does the quarter produce a visible win early, and is “done” defined before the work begins?
Restraint Is the rewrite argued against with numbers, and is the condition for reconsidering it stated?

Where you now stand

You can now recognize complexity by its symptoms, design modules deep enough to hide their hard cases, and change structure in small, named steps kept apart from changes in behavior. You can find technical debt in version control and record it with interest, owners and triggers. You can change code that has no tests by characterizing it first and sprouting new behavior beside it, and you can reason about change across a large codebase, from Hyrum’s law inside the company to one version of each dependency. All of it rests on tests, and the characterization tests that made the barcode issuer safe to change are not yet a strategy. Chapter 3 asks what a test is worth, and how a suite is designed so that it protects the five fairness invariants of Verified Resale without becoming the next thing nobody dares to change.

References