Originally published at tddbuddy.com.
This is the first post in the three-part Vocabulary Is the Product arc. The follow-ups are Agents Amplify Whatever Vocabulary They Find and Product Literacy Is the New Core Engineering Skill.
The visible output of TDD was always the wrong thing to measure.
Teams pointed at the green bar, the coverage percentage, the regression safety net. All real. All undersold what was actually accumulating inside the test suite while the team ran the loop. The passing tests were the receipt. The vocabulary was the asset.
Every well-named builder method, every domain type, every scenario name was a small act of product thinking crystallized into a callable identifier. By the time a mature TDD codebase had a few thousand tests, it also had a working dictionary of the system's concepts: what exists, what it's called, and how concepts relate. Nobody called it that. Nobody tracked it. It accreted quietly in the test helpers and the factory methods and the scenario names, and it was the most durable thing the team ever built.
What You Thought You Were Producing
The honest answer is that most TDD practitioners believed the output was the test suite. Specifically: passing tests, regression coverage, and the refactoring safety net that comes when a suite is green and comprehensive.
Those benefits are real. Regression coverage means changes are visible before they ship. A green suite means the refactor step has a net. The safety net matters. It's what lets teams keep moving as the codebase grows. The teams that pitched TDD to skeptical management pitched these benefits because they were measurable. They showed up in bug counts and deployment frequency and time-in-QA. The pitch worked often enough that TDD got adopted, then re-adopted, then adopted again after the first adoption decayed.
But calling those benefits the "output" is like calling the printed page the output of a writing process. The page matters. The ideas the page carries are the point.
The loop produced discipline over naming, shape, and intent on every commit. Red forced a name before implementation existed. Green forced a minimal implementation against that name. Refactor forced the implementation and its name into alignment with the rest of the system. Each step was a constraint on the form of the output, not just on its correctness. Three constraints per commit, thousands of commits over the life of the codebase, and what accumulated was not just a suite of passing tests. What accumulated was a shape. A structure. A set of names that had been pressure-tested against the domain and sharpened by a thousand tiny review loops.
That discipline accumulated. The accumulation had a form. The form was a vocabulary: a set of named concepts, expressed as types, builders, and scenarios, that encoded the team's shared understanding of the system.
The tests were the container. The vocabulary was the contents.
What You Were Actually Producing Was a Vocabulary
Run the loop a thousand times on a single codebase and look at what's left. Not the assertions. Look at the names.
There is a controlled vocabulary: a deliberate, curated set of names for domain concepts. Not every string or variable in the production code, but the terms that appear repeatedly in test code because they represent things the domain cares about. LoyaltyMember, not CustomerType == 1. Money, not decimal. aCartReadyForCheckout(), not a twelve-line setup block.
Every test data builder was a vocabulary entry. Every scenario name was a vocabulary entry. Every domain type was a vocabulary entry. Each one represented a decision: this concept exists in this system, and this is what we call it.
The decision had to be made before the test could be written. Red forces the question. To write a failing test for a behavior, the behavior has to be named. The naming is the first act of design. The implementation that follows is secondary.
Consider the same scenario written three ways:
// WEAK: no vocabulary, only machinery
var c = new Customer();
c.Type = 1;
var o = new Order();
o.Total = 60m;
var d = new DiscountService().Calculate(c, o);
Assert.Equal(6m, d);
// MIDDLING: types exist, but the scenario is still assembly
var customer = new Customer { LoyaltyTier = LoyaltyTier.Gold };
var order = new Order(60.00m, customer);
var discount = new DiscountService().Calculate(customer, order);
discount.Should().Be(6.00m);
// STRONG: the test is a sentence in the system's language
[Fact]
public void Gold_loyalty_members_get_ten_percent_off_orders_over_fifty_dollars()
{
var order = anOrder()
.forCustomer(aLoyaltyMember().withGoldStatus())
.containing(anItemCosting(60.dollars()));
var receipt = checkout.Process(order);
receipt.Discount.Should().Be(6.dollars());
}
The third version does not just pass the same test as the first. It carries a different vocabulary. aLoyaltyMember(), withGoldStatus(), anItemCosting(), 60.dollars(), receipt.Discount: these are not just more readable names. They are vocabulary entries. They are decisions about what concepts exist and what the domain calls them. The test body is a sentence in the system's language because the team built a language for the system.
The first version has no vocabulary. It has primitives and magic numbers. The magic number 1 means something, but nothing in the test says what. The decimal 60m means something, but nothing calls it out as a threshold. The implementation might be correct. The vocabulary is absent.
The controlled vocabulary the team built by writing the third kind of test was the actual durable output. The test suite was the place it lived.
Vocabulary Outlives the Code
Implementation has a short life. Frameworks change. Languages get replaced. Runtimes get upgraded. The code that was correct in 2015 has been rewritten two or three times since then, migrated to containers, moved to a new cloud provider, and touched by twenty developers who were not there when the original design was made.
Vocabulary has a long life.
The concepts that mattered to the business in 2015 still matter. "Loyalty member" still means something. "Free shipping threshold" still means something. "First-year member" still means something. What changed was the implementation. What persists is the set of concepts and the names the team chose for them.
The persistence is not accidental. Business domains are stable in ways that technology is not. The rules governing whether a customer qualifies for free shipping change slowly and deliberately, negotiated with stakeholders, reflected in policy documents. The framework the application runs on changes because a new version came out, a security patch required it, the cloud provider deprecated it, or a new hire preferred something else. The domain outlives the stack. The vocabulary that encoded the domain should outlive the stack too. It does, when the test suite was the place it lived.
This is not a theoretical point. Watch what happens when a team rewrites a service in a new language or framework. Two paths diverge.
The first team carries the vocabulary. They extract the domain types, the builder methods, the scenario names, and the factory hierarchies before they start writing the new implementation. The new codebase looks different internally, but the concepts are the same and the names are the same. Developers who know the old system can navigate the new one because the vocabulary is the same. The test suite for the new system reads like a translation of the old test suite, not a reimagining. The system survives the rewrite.
The second team keeps the code and loses the vocabulary. They refactor with autocorrect, copy logic blocks, rename things to match the new framework's conventions, and gradually drift from the domain language the business still uses. Six months later, a product manager says "loyalty member" and the developers say "that's UserRank == 2 in the new system" and that's when everyone discovers how much tacit vocabulary was lost. The domain knowledge that lived in the test factories and the scenario names did not survive the migration. The system got rewritten. The system also got lost.
A team that rewrites a service and keeps the vocabulary keeps the system. A team that keeps the code and loses the vocabulary lost the system.
The code was always replaceable. The vocabulary was the moat.
Builders Are the Lexicon
A test data builder is not a convenience for avoiding boilerplate. It is a grammar.
Every builder method is a verb or modifier in the domain language. forCustomer() is a verb. withGoldStatus() is a modifier. inTheirFirstYear() is a modifier that encodes a temporal concept the business cares about. Every parameter type is a noun: Money is a noun, LoyaltyTier is a noun, Address is a noun. Stack them and you have a grammar: subject, qualifiers, action, assertion. The grammar is not arbitrary. It mirrors the grammar the business uses when it talks about the system.
That mirroring is the point. When the builder API and the business language are the same, new requirements enter the codebase without translation. A product manager says "first-year loyalty member," and a developer types aLoyaltyMember().inTheirFirstYear(), and the phrase compiles. The concept maps directly because someone, during a red step months earlier, was forced to name the concept before implementing it and chose the domain's word over a technical shortcut.
When that mapping breaks, the cost is invisible at first and catastrophic later. The product manager says "first-year loyalty member" and the developer translates it mentally to CustomerTenure.New and two weeks later there's a support ticket about "new customers" getting a benefit they shouldn't because "new" and "first-year" turned out to mean different things and no one noticed the concepts had drifted apart.
To compose a test scenario using a rich builder API is to write a sentence in the system's language. The sentence has structure: subject (who is acting), context (what conditions apply), and event (what action is being taken). Reading a test body out loud is reading a sentence in the domain language. If the sentence doesn't parse, the builder API is wrong, or the domain model is wrong, and the test is telling you so before the implementation gets a chance to hide it.
var order = anOrder()
.forCustomer(aLoyaltyMember().inTheirFirstYear())
.containing(anItemCosting(75.dollars()))
.duringBlackFriday();
var receipt = checkout.Process(order);
receipt.Discount.Should().Be(15.dollars());
Read that out loud: "An order for a first-year loyalty member containing an item costing seventy-five dollars, placed during Black Friday, should produce a fifteen-dollar discount." That is an English sentence. It is also a C# expression. The builder API made those two things the same thing.
Now compare to what happens when the vocabulary is weak and the team needs to rename a core concept. Say the business decides that "loyalty member" should be called "rewards member" everywhere. In the strong version above, the change is one rename: the aLoyaltyMember() factory. Every test that uses it updates in the same refactor sweep. The compiler finds every call site. Nothing is missed.
In the weak version:
// Every scattered usage needs a manual grep
var c = new Customer();
c.Type = 1; // Is 1 a loyalty member? A rewards member? Both?
The magic number 1 cannot be found by a rename. There is no token to rename. The concept exists only as a convention scattered across dozens of files. The refactor becomes archaeology: find every place where Type == 1 appears and determine whether it means the concept that just got renamed. Some of those usages are loyalty. Some are something else. Nobody is sure. The rename takes a week and still misses edge cases in a reporting module nobody touched.
The third vocabulary state rewrites itself. The first vocabulary state needs grep and hope.
The Vocabulary Was Invisible Because Nothing Tracked It
Here is the design failure at the center of the TDD conversation for twenty years: the vocabulary was the output, and nothing ever made that visible.
There was no file called domain-language.md. No lexicon checked into the repo. No metric that measured vocabulary health. No CI gate that failed when a new primitive type snuck into a test where a domain type should have been. The vocabulary accreted inside the test suite as a tacit norm: the senior developer knew that aLoyaltyMember() was the right factory and new Customer { Type = 1 } was the wrong approach, and they would mention it in code review if they caught it, and they would miss it roughly half the time because the diff was long and the type mismatch was subtle.
The vocabulary was real. It was load-bearing. It was completely invisible to organizational processes.
So nobody talked about it as an asset. When teams discussed the business case for TDD, they talked about bug catch rates and regression coverage and time saved in QA. They did not say "we have been building a domain-specific language for the system and it lives in our test factories and every new developer who reads it learns how the business thinks about the problem." That would have sounded either pretentious or vague. "Our coverage went from 40% to 80%" sounded concrete.
The coverage number was trackable. The vocabulary was not. The coverage number got the business case. The vocabulary got ignored.
And so, over time, vocabulary erosion was also invisible. A factory got duplicated with a slightly different name. A scenario name drifted toward implementation language. A domain type got replaced by a string because a junior developer was in a hurry and the review didn't catch it. Each erosion was small. The cumulative effect was a test suite where the vocabulary was no longer trustworthy, where two factories existed for the same concept with slightly different setups, where scenario names mixed business language and technical language in the same sentence.
Nobody noticed the asset depreciating. They only noticed, eventually, that the codebase felt harder to work with than it used to.
The Test Suite Is the Controlled Vocabulary
Stop thinking of the test suite as a safety net. Start thinking of it as a living lexicon.
The test suite is the controlled vocabulary of the system. Every builder method is an entry. Every domain type is an entry. Every scenario name is an entry. The vocabulary is controlled in the sense that it was decided upon: someone chose aLoyaltyMember() over new Customer { Rank = 2 }, and that choice encoded a judgment about what concept matters to the domain and what the domain calls it.
The vocabulary is the moat.
This is not a metaphor. A codebase with a rich, stable, expressive vocabulary is genuinely harder to replace than one without. The first time a team tries to port it, migrate it, or hand it off to a new team, the vocabulary is what carries the domain knowledge. The new team reads the test suite and learns the system. They learn what a "loyalty member" is, how a "first-year" window is defined, what qualifies as an "order over threshold." They learn this from the code because the code uses the domain's words.
The domain words are doing work documentation cannot do. A wiki page that says "loyalty members are customers who have signed up for the rewards program" is a sentence. aLoyaltyMember().inTheirFirstYear() is a sentence plus a type system plus a suite of scenario tests that describe exactly how "first year" is calculated, what happens at the boundary, and what changes when the policy changes. The vocabulary entry is executable. The wiki page is not. The vocabulary entry is current because it has to be current to compile. The wiki page is current until someone forgets to update it, which is always.
A codebase without vocabulary has to be explained. Every handoff requires tribal knowledge transfer. Every migration requires someone who was there when the original decisions were made. The vocabulary that was never built into the tests has to be reconstructed from memory, which is expensive, slow, and inaccurate.
Every refactor that strengthens names strengthens the asset. Every drift weakens it.
The refactor step in TDD was not just about tidying the implementation. It was about maintaining the vocabulary. When a new concept appeared in an implementation and its name was fuzzy, the refactor step was the moment to sharpen it. When two builders were doing nearly the same thing under slightly different names, the refactor step was the moment to collapse them. When a scenario name described the mechanism instead of the intent, the refactor step was the moment to rename it.
The teams that cheated on the refactor step did not just get tangled implementations. They got vocabulary drift. They got a test suite where the names slowly stopped meaning what the domain meant. They got a lexicon with entries that contradicted each other, that overlapped without explanation, that mixed languages without intention.
That is the real cost of skipping refactor. Not just messier code. A degraded asset.
The Output Was Always a Language
TDD practitioners spent twenty years making the case for the green bar.
The green bar matters. But the green bar was never the point. The green bar was the constraint that forced the naming. The constraint that forced the naming built the vocabulary. The vocabulary was the product.
The test suite is not a safety net draped over the production code. It is the place where the team built a language for the system, commit by commit, factory method by factory method, scenario name by scenario name. The production code is the back-translation: the implementation that had to satisfy the language the tests defined.
Read a well-maintained TDD codebase's test suite and read the system's domain model. They are the same document. The test suite is the specification, the glossary, and the grammar, all compiled, all executable, all current. The production code proves the specification is achievable. The test suite is the specification itself.
The visible output of TDD was a passing test suite. The actual output, the one that compounded value over the life of the codebase, was the vocabulary the team built while writing it. Every test that forced a naming decision, every factory that encoded a domain concept, every scenario that pinned business intent to a callable identifier: these were the asset accumulating. The green bar was just proof the asset was still intact.
That vocabulary is now the interface agents compose against. It is the controlled surface through which the next generation of tools reads the system. Teams that built it will find their agents producing coherent, domain-appropriate output. Teams that skipped it will find their agents producing faster primitives.
The hidden output of TDD was never code. It was always the language.
Top comments (0)