Part 2 — Essential and Accidental Complexity
Part 1's answer to that question was 'you can't' — and that still stands. No controlled counterfactual exists, and it isn't coming. What's fixable is narrower: not whether you can verify quality, but what you build when you accept you can't.
The Ferrari That Plows the Field
Picture a user story: "As a farm owner, I need my field plowed."
Someone unfamiliar with farming, handed only that sentence, could reasonably reach for the most capable, most desirable, most obviously powerful means of getting across a field fast: a Ferrari. It satisfies the stated behavior. It moves. It's fast. Nothing in the sentence says it's wrong.
To a farmer, it's a ridiculous choice — no tow hook, no ground clearance, no grooved tires for mud, a chassis that will bottom out on the first furrow. But none of that is in the story. It was never going to be. The farmer didn't leave it out through carelessness. It's tacit — decades of lived experience about soil, terrain, and machinery that never had to be stated because, to the farmer, it goes without saying. We don't make this mistake with tractors specifically, because tractors are common enough that everyone has already absorbed the association. But swap the domain for something less familiar — a car mechanic parachuted into farm equipment, an engineer parachuted into logistics or lending or insurance — and the Ferrari mistake stops being obvious and starts being the default outcome.
This is the risk sitting underneath every request phrased as behavior: plow the field, approve the loan, route the shipment. Each one is satisfiable by something that technically works and is completely wrong for the domain it has to operate in — and the gap between those two doesn't show up in the sentence. It shows up later, in a field that got plowed fast and shallow because nobody told the machine, or the engineer, that depth mattered more than speed.
Why Requirements Can't Be Perfect — By Definition
There's a comfortable assumption underneath a lot of software process, waterfall included: that if the domain expert just writes the requirement carefully enough, up front, the ambiguity goes away. Get the specification right, then build to it.
This isn't a matter of trying harder, and it's worth being precise about why. The farmer isn't withholding the ground-clearance requirement. He doesn't know it's missing. Knowing what needs to be said requires knowing what the listener doesn't already know — and the farmer has no way to model that, because he's never had to. Everyone he's ever talked to about plowing already knew what a field needs. The gap only exists between him and someone who doesn't share his history, and he can't see the gap from his side of it, because from his side there isn't one.
This is a different claim than "requirements are often incomplete because people are rushed or careless." Even a patient, careful, engaged domain expert cannot fully specify a domain to someone outside it, because the missing information isn't information he's choosing not to share — it's information he doesn't know he has. It only becomes visible in contact with someone who lacks it, which means it can only surface during the conversation, or during the build, never fully in advance. A requirement gathered once, however carefully, is a snapshot of what one person thought to say to another person, not a complete transfer of a domain. It can't be, by the nature of what tacit knowledge is.
That's the actual argument against waterfall, for domains like this one — not that big upfront design is inconvenient or slow, but that for a business domain built substantially from tacit knowledge, "complete upfront understanding" isn't achievable in practice, for any domain expert, no matter how good. There will be domains that can be specified more precisely in advance, but it's doubtful even those can arrive at a fully correct and complete picture in version one. More time invested up front gets you closer, and leaves less to correct later, but the picture only gets validated through contact with the real system and the feedback loop that comes with building it. What changes by domain is how much iteration is needed, not whether it's needed at all — and for most business software, that need is large enough that shortening the loop matters more than lengthening the upfront phase. If the goal is to reduce the risk of building the Ferrari, the only real lever is shortening the distance between the requirement being stated and the mismatch being discovered — not writing better requirements up front.
Why the Farmer Never Needed a Model — and the Engineer Does
Here's the detail worth sitting with: the farmer has never built a model of his own domain. He doesn't have a diagram of soil types, terrain gradients, and their effect on plow depth. He doesn't need one. He simply does the domain — years of accumulated pattern-matching that never had to be made explicit because it was never being transferred to anyone.
The engineer is in the opposite position. They have no lived history with the domain, and they're being asked to produce something that behaves correctly inside it anyway. A model — the actual object of domain modeling — isn't paperwork layered on top of the "real" work of writing code. It's a substitute for the lived intuition the engineer doesn't have and can't acquire the way the farmer did, by doing the job for twenty years. It's the compressed, explicit version of what the domain expert holds tacitly, built because the person who needs to act correctly in this domain doesn't have the history that would let them act correctly without it.
This reframes what a domain model is for. It isn't there to make the code look organized. It exists because the engineer's ignorance of the domain is the default starting condition, and the model is the mechanism that closes that gap enough to build something that survives contact with the field.
The Model Talks Back
There's a second function a domain model performs that's easy to miss if it's only thought of as documentation that happens to run: it becomes the actual medium of conversation with the domain expert.
Show a farmer a diagram of Field, Terrain, PlowDepth, and SoilCondition, and something happens that a user story never achieves. The farmer recognizes it — or doesn't. If a concept is named wrong, missing, or conflated with something that should be separate, he sees it immediately, the way any expert notices a wrong description of their own domain on sight. He doesn't need to read code. He needs to recognize the shape of his own field of work, and either nod or correct it.
That makes the model more than a private substitute for the lived experience the engineer doesn't have. It becomes a shared artifact — a way for the domain expert to correct what's wrong and add what's missing, in a form neither side had to translate through unrelated technical vocabulary first. It's the same discovery process from earlier in this series, except now there's something concrete to point at instead of only a conversation to have. That's the difference between discovery as a one-time interview and discovery as something a project keeps doing throughout its life: the model gives the domain expert something to react to, and every reaction is new information the model can absorb.
The Sundial and the Clock
There's a cleaner way to see the difference between satisfying a requirement and understanding a domain, and it doesn't need farming at all.
Someone asks what time it is. The fast way to answer, once, is to look at the sun's position and work it out — a stick in the ground, a shadow, a quick calculation. It's correct. It satisfies the request. It took almost no effort.
It also only works at that moment, under those conditions. Ask again at night. Ask on a cloudy day. Ask indoors. The stick-in-the-ground approach doesn't fail because it was built badly — it fails because it was never a model of how time works. It was a calculation performed once, for the specific conditions present when the question was asked, dressed up as an answer to a more general question than the one it can actually address.
A clock is a different kind of thing entirely. Nobody asked it what time it is right now — it was built to encode the actual mechanism, gears turning at a known rate, and it keeps producing the correct answer regardless of sun, cloud, or time of day, including for moments nobody thought to test it against. The clock isn't smarter than the sundial. It isn't even answering a different question. It's just built from the mechanism instead of from the answer, and that's the entire difference.
This is the real distinction underneath "satisfying behavior" versus "building a model" — not a language choice, not procedural code versus object-oriented code, since either can be written either way. The sundial approach calculates the specific answer a specific requirement asked for, at the moment it was asked, and stops there. The model approach encodes the relevant mechanism that produces the answer, so that it keeps producing correct answers under conditions nobody explicitly enumerated. A green test suite is a sundial reading — proof the answer was right at noon, on a clear day, for the case that got tested. It says nothing about midnight, or rain, or the question nobody thought to ask yet. This is also why the library article's LendableItem example gave you loan-frequency reports and reorder signals nobody requested: not a bonus feature, just what a correct mechanism does once it's built — it keeps being correct for cases it was never specifically asked about, the same way a clock keeps telling time nobody checked it against.
This is worth stating plainly, because it's easy to misread as an edge-case problem rather than what it actually is. If a sundial-style implementation only ever gets checked during an MVP's daytime hours, its tests pass — cleanly, repeatedly, every time. The mechanism looks solid, because nothing has contradicted it yet. It isn't solid. It was never validated against the domain, only against the sliver of it the current tests happened to cover. When the requirement expands to round-the-clock, the failure isn't a missing case to patch in. The entire mechanism was built for the wrong problem, and none of it survives the expansion intact — because there was never a mechanism there to begin with, only an answer that happened to be correct once.
That's the part easy to miss about a domain model: it isn't documentation of understanding, sitting beside the code as a reference. It's the engine that generates correct behavior going forward, including for the parts of the domain nobody has asked about yet.
Naming It: Essential and Accidental Complexity
Fred Brooks named this distinction in 1986, in No Silver Bullet, and it's worth using his terms directly rather than reinventing them, because the distinction predates every architectural trend either side of any debate could otherwise appeal to.
Essential complexity is the complexity inherent in the problem itself — the rules, relationships, and constraints that exist regardless of how the problem gets implemented: soil type and terrain in farming, the mechanics of timekeeping, gear ratios in an engine. Accidental complexity is complexity introduced by whatever representation and technology gets chosen to implement that problem: a framework, a deployment topology, the specific way a service happens to be split from another. Essential complexity cannot be removed without changing what the system does. Accidental complexity exists purely to make the essential complexity operational, and none of it should be allowed to dictate what the essential complexity is allowed to look like.
The sundial and the Ferrari are both implementations that ignore the essential complexity of their domains — they satisfy the stated requirement while encoding none of the mechanism underneath it. The clock and the tractor are implementations that respect it: built from what the domain actually requires, which is why they keep working outside the exact conditions of the original ask.
Why This Compounds With Scale
For a small system, none of this costs much. A handful of concepts, a single developer who can hold the whole domain in their head — the difference between a sundial and a clock barely registers, because nobody's asking the hard question yet.
It stops being free for two separate reasons once a system grows.
The first is contradiction-spotting. In a small system, if two pieces of logic quietly disagree about what a "loan" or a "position" means, one person notices, because one person can still see both pieces at once. Past a certain size, nobody can see both pieces at once — which means an essential concept can silently diverge into two or three slightly different meanings across the system, and nothing surfaces the contradiction until it produces an inconsistency somebody has to explain after the fact.
The second is the bounded-context problem from Part 1, restated: a human mind can hold only so much structure at once, the same way any representation has a limit to how much a person can carry in working memory. When the essential complexity is explicit — named, owned, in one place — a person only has to load the relevant piece to reason about a change. When it's dissolved into accidental structure, reasoning about any one change requires reconstructing understanding scattered across services, layers, and naming conventions that don't map to the domain at all.
Premature Boundaries Are Waterfall, Applied to Structure
There's a specific version of this mistake worth calling out on its own: drawing service or module boundaries before the domain's actual contours are visible.
Imagine splitting an engine department from a transmission department early in a car's design, based on the first impression that they're separate concerns with a clean interface — say, a shared notion of rotational speed. Only later does it become clear the two are coupled through torque, not speed, and that the "clean" interface drawn on day one was never the seam the domain actually has. Correcting that after the fact doesn't mean editing a diagram. It means unwinding a boundary that's since been built on, deployed around, and staffed for — two teams, two codebases, two deployment pipelines, all organized around a seam that turned out to be in the wrong place.
This is the same mistake as waterfall, one level up. Waterfall assumes the domain can be fully specified before any code exists. Premature service decomposition assumes the domain's boundaries can be fully known before any of it has been built and tested against reality — and boundaries are exactly the kind of thing that only reveals itself under contact, the same way the torque coupling only shows up once the engine and transmission actually have to work together.
It's worth being precise here rather than sweeping: this isn't an argument that distributed architecture is wrong. Microservices can have a legitimate place. It's a much narrower one than the default status they're often given in "enterprise development" today, where reaching for them has become closer to a starting assumption than a decision made in response to anything the domain actually demonstrated.
The lesson isn't "never split." It's that a boundary drawn before the domain's contours are known is a bet, not a finding — and bets made early, staffed and deployed around, are exactly the kind of decision this whole series has been describing: cheap to make, and expensive to unmake, precisely because nothing about splitting early announces itself as a mistake until the wrong seam has already been built on for years.
Where This Leaves Us
The Ferrari, the sundial, and the engine split early are the same failure, three times over: something that satisfies what was asked, built without the mechanism that would let it keep being correct once conditions change. The fix isn't more upfront specification — that's unavailable for the same reason perfect requirements are: tacit knowledge, not effort, is the limiting factor.
The obvious mistake sitting underneath most long-lived systems is optimizing for the wrong finish line. Shipping working behavior gets treated as the goal, when what actually determines whether a system survives is something else: how fast a bug gets found and fixed, how easily new understanding gets absorbed without a rewrite, how quickly a new hire can trace what happens where. Those aren't secondary qualities layered on top of "does it work." They're the entire difference between a system still running, and still comprehensible, twenty years from now, and one quietly marked for a rewrite every five to seven years because nobody can afford to keep changing it.
What actually produces that difference isn't a methodology. It's the discovery of the essential complexity — the actual mechanism, found the way the sections above describe, not assumed on day one — and encoding it explicitly, in one place, as a model.
Once that happens, something inverts that's easy to miss. Accidental complexity — the framework, the deployment topology, whatever technology happens to run any of it — starts adhering to the model, instead of the model quietly bending to fit whatever the framework assumed the domain should look like. That's the direction the dependency is supposed to run: technology in service of the domain, replaceable and swappable, without ever touching the part that represents what the business actually is. When it runs the other way — domain bent around framework convention — the technology becomes load-bearing, and a framework migration stops being an engineering project and becomes a rewrite.
None of this requires getting the model right the first time — that's the same impossible demand as perfect upfront requirements, just moved one layer down. What it requires is a model that can be corrected, and scattered logic isn't correctable in the same sense, whatever it looks like from the outside. Changing it isn't a controlled correction, because nobody can fully trace what happens where without a model to trace it against — every change to logic spread across services and layers is closer to a gamble than a certainty, a bet that this was the only place the old assumption got encoded. A model doesn't eliminate that risk. It's what makes the risk visible and containable instead of silent and compounding, and it's what lets a wrong assumption discovered in year three cost a week instead of a quarter.
That inversion — model as the fixed point, tooling as the replaceable part — is what lets a system's core outlive any single technology choice, any framework version, any team that happens to be maintaining it this year. And the fact that a model exists at all, however incomplete, is already evidence that someone attempted to understand what the domain actually is, rather than only what it was asked to do. Not documentation. The engine — and the only part of the system built to still be correct, and still be there, decades from now.
Correct behavior is part of quality. The machine that produces that behavior — the part with no reference class, nothing to compare it against — is the much larger part, and the part nobody can see. Just because it can't be tested in a CI/CD pipeline doesn't mean it isn't there. It's the actual driver of cost and stability, and over the long run, those are what decide whether an application survives.