DEV Community

Cover image for Discovery and Fit Testing: The Two Jobs of a Domain Model
Leon Pennings
Leon Pennings

Posted on • Originally published at blog.leonpennings.com

Discovery and Fit Testing: The Two Jobs of a Domain Model

Every experienced architect has had this happen. A domain expert asks for feature X. An hour of conversation later, it turns out they don't actually need X at all — they need something X happened to be the only tool they knew for. Months after that, while building the thing they actually needed, the code starts fighting you: a special case here, a workaround there, a class that has to reach somewhere it shouldn't. Those two moments feel unrelated. They're not. They're the same activity, happening at two different points in time.

Call the first one discovery: finding out what the domain actually is by refusing to accept a stated want as the real need. Call the second one fit testing: using implementation itself as the check on whether that theoretical model actually holds up against the real thing it's supposed to represent. A rich domain model is not a coding style. It's the artifact that sits between these two — built by the first, tested against reality by the second.

Discovery: Finding Out What the Domain Actually Is

Domain experts are precise about their domain and imprecise about their own requirements, and this isn't a criticism — it's structural. Someone who has done a job fluently for years has usually stopped noticing which parts of how they do it are essential and which parts are habit or inherited tooling. Ask a metadata specialist who has spent a career hand-writing XPath what they need from a new tool, and the answer arrives instantly and confidently: XPath support. That answer is true. It is also the current solution, offered back as if it were the problem.

Discovery is the work of not stopping there. Ask what the request is trying to achieve. Ask what happens if that specific mechanism isn't available. Ask what the same outcome looks like described without reference to any particular tool. Somewhere under three or four of these questions, the actual need usually surfaces — not "give me XPath," but "let me identify a piece of a structured document without first having to learn a query language." That reframing changes what gets built: a hierarchy of named parts that mirrors how anyone would describe the document out loud, with XPath kept as an escape hatch for the minority who want it, instead of the interface the whole tool is built around.

A request is a fact about the domain — proof that some need exists — not a specification of the correct implementation. That's the whole method, really. The rest is just the discipline to keep asking past the first fluent answer, and enough earned standing that the fourth question lands as insight rather than as second-guessing an expert on their own domain.

Fit Testing: The Code Is the Reality Check

Once a theoretical model exists, it has to be built — and this is where a different kind of test takes over. Not a unit test. A unit test verifies that a given input produces a given output, and it will pass just as happily against a model that took three workarounds to get there as it will against one that didn't. It has no opinion on the workarounds. A fit test does: did this shape fall into place naturally, or did the theory have to be bent to survive contact with the real thing it was supposed to describe?

Take an overdue invoice that needs to affect a customer's account. The fast implementation has Invoice reach into Account and change its status directly. It works. It also requires Invoice to know something about Account that was never its business to know — and that mismatch shows up as friction: an import and a method call that cross a boundary the model was supposed to respect. The corrected version has Invoice record a CustomerFact on Account; Account alone decides, from its own history of facts, whether a status change is warranted. Invoice never learns blocking exists. Account never learns anything about invoices, only about facts. Nothing had to be bent to make it work — and that absence of bending is the fit test passing. A green test suite tells you the code returns the right answer. A friction-free implementation tells you the theory actually fit the real domain. Those are different claims, however often they get treated as one.

This shows up anywhere code starts accumulating branches instead of concepts, not just in the classic entity-relationship examples:

if (customer.isVIP())
    ...
else if (customer.wasVIP())
    ...
else if (customer.isTemporaryVIP())
    ...
Enter fullscreen mode Exit fullscreen mode

The common response is "add another flag, another enum value, another branch." The fit-testing response is a different question: why am I accumulating special cases instead of finding the concept that was missing — a MembershipStatus, a BenefitsPolicy, something that already explains all three cases as states of one idea instead of three unrelated flags bolted on as they came up. The same signal shows up as a Controller, a Processor, or an Orchestrator materializing to coordinate behavior that no single object seems to own; as a Map standing in for a type nobody named yet; as a Strategy hierarchy invented purely to keep something "flexible" rather than because the domain actually varies that way. None of these are technically wrong. All of them are the same tell: a concept is missing, and something generic has been built to cover for its absence.

The whiteboard version of a model and the code version should be the same shape, box for box, arrow for arrow. When they diverge — when four clean concepts on paper need three extra abstractions before they can become four collaborating objects — that divergence is the fit test already failing, before a single unit test has been written.

There's a second, quieter gain that fit testing produces almost as a side effect, and it's worth naming directly because it doesn't show up on any dashboard: when the model actually fits, the engineer building it ends up understanding the business, not just the ticket. Following a model that fits reality means following the same shape the business itself already has. An engineer who's built CustomerShipping, or CustomerFact, or LendableItem understands what those things mean in the business, not just what fields they carry. That shouldn't be a controversial claim, but it's rarely stated plainly: engineers who understand what they're actually automating make fewer mistakes, catch wrong requirements before they ship instead of after, and are visibly more engaged in the work than engineers translating a ticket into code they don't have to understand to close it. Fit testing isn't just how you check the model. It's how the team ends up actually knowing the business it's building for.

Why These Are Two Skills, Not One

Discovery is closer to a research-interview skill than a technical one — listening for the gap between a stated want and an underlying need, and having built enough credibility that pushing back on a domain expert lands as expertise rather than obstruction. None of that depends on a language or a framework. It's earned slowly, one correctly-challenged assumption at a time.

Fit testing is a technical habit of noticing resistance and refusing to route around it — asking, every time a shortcut presents itself, what concept its absence is covering for. It's learnable in a way discovery isn't; it's closer to a discipline that improves with deliberate attention than to an interpersonal skill that has to be earned in real time with a real person.

Most engineers who are strong at one are mediocre at the other, which is part of why the combination looks rarer than either skill actually is on its own. Frameworks make it worse by supplying a default shape, removing the need to properly investigate a request before reaching for whatever pattern already exists for requests roughly like it.

Where Discovery Went

Ask most teams what "design" means at the start of a project and the answer, in practice, is a technology decision. Spring or not. Event-driven or request-response. Microservices or a monolith. CQRS or plain CRUD. These conversations happen early, they happen with real seniority in the room, and they get treated as the design phase — the moment the important architectural thinking supposedly takes place.

None of it is design. It's procurement. Design, properly understood, is the work of figuring out what the core automation is actually supposed to do — what the business logic is, what the concepts are, how they interact, who owns which decision. Technology is supposed to answer to that understanding, not substitute for it. A framework, a message bus, a deployment topology — all of it exists in service of the business logic once that logic is understood. None of it can tell you what the logic is. Only discovery can do that, and discovery is precisely the step that gets skipped when "design" has already been spent on choosing a stack.

This isn't a minor sequencing mistake. Choosing the technology before the domain is understood guarantees the domain gets bent to fit whatever was chosen, rather than the other way around. A team that has already committed to microservices has already, silently, committed to wherever the service boundaries end up being drawn — before anyone has done the work of finding out whether the business actually has boundaries there at all. The technology conversation feels like design because it's substantive, technical, and happens among senior people. It produces artifacts — diagrams, ADRs, a slide deck. Discovery, by comparison, produces a shared understanding that's much harder to put in a deck, which may be exactly why it's easier for an organization to skip: it doesn't look like work in the way a technology decision does, even though it's the harder and more consequential decision of the two.

What AI Changes, and What It Doesn't

AI-generated code doesn't experience friction. A three-way join across concepts that never should have touched gets written with exactly the same fluency as a clean one-concept query. The resistance that used to nudge a developer toward asking whether the model was wrong doesn't register anywhere in the process — not because the tool is careless, but because friction was never really "the thing that mattered." It was a side effect of implementation being expensive enough to hurt. Remove the cost and the alarm goes quiet. The question it used to raise is exactly as important as it always was.

Fit testing now has to be a deliberate habit instead of something that used to announce itself as pain. Discovery, if anything, gets more valuable, not less — it's the one part of this that was never about code in the first place. It happens in a conversation, built on trust that took time to earn, and nothing about implementation being free changes how that trust gets built or removes the need for someone to ask the fourth question that gets past the tool being asked for and down to the need underneath it.

So What

The best domain models don't read like clever engineering. They read like something a child could follow — Mickey Mouse, not general relativity. That's not a lack of ambition. It's the actual goal. General relativity is undeniably more impressive to look at, and it is also vastly harder to maintain, harder to hand to the next person, and far more expensive every time it turns out to be slightly wrong. The closer a model gets to correct, the more boring and obvious the resulting code looks — no controller coordinating things that don't know about each other, no orchestrator standing in for a decision nobody assigned an owner to, just a small number of objects doing exactly the thing their name says they do. Impressive code is usually a sign that a concept is still missing. Boring code is usually a sign that it isn't.

This has an uncomfortable corollary worth saying plainly, because it's the part nobody warns you about: correct modelling work is invisible by design. The whole point of getting the model right is that the result looks like it wasn't hard — which means the people doing it well are structurally at risk of looking like they did less than whoever is maintaining the impressive, complicated, wrong version next door. Nobody throws a project a parade for being simple. The praise goes to whoever is visibly wrestling something difficult, even when the difficulty was optional.

Software architecture isn't primarily about drawing better class diagrams. It's about asking better questions before the code exists, and paying attention when the code quietly tells you those answers were wrong.

Top comments (0)