I want to talk about a bug that, on the surface, looked trivial. An endpoint was missing. A frontend was sending the wrong ID. A "space" was showing up twice in a list that should have had unique entries. Individually, each of these looked like the kind of thing you fix in five minutes and forget about by lunch.
But when I sat with all three at once, a pattern emerged that had nothing to do with any single line of code. The system had, somewhere along the way, decided that the same real-world thing was actually two different things — and it had been quietly building parallel infrastructure to support that fiction ever since.
The door analogy
The best way I found to explain this to my team was with a door.
Imagine a door with two ways to use it: you push a handle to walk in, and you lean on a bar to walk out. Reasonable enough — lots of doors work that way. Now imagine that instead of being one door with two interaction mechanisms, someone decided, early in the building's design, that "the door you enter through" and "the door you exit through" were two separate doors. Not two mechanisms on one door — two entirely distinct doors, each requiring its own hinges, its own frame, its own maintenance schedule, its own set of keys.
They happen to occupy the same opening in the wall. But structurally, in the blueprint, they are unrelated.
Now every time you want to add a feature to "the door" — say, a lock, a window, a bell — you have to remember to add it twice, once to each door, because the blueprint has no concept of "the door" as a single thing. It only has "the door you enter through" and "the door you exit through." If you forget to add the feature to one of them, nothing in the system tells you. It just quietly doesn't work on that side.
This is what had happened, conceptually, in the codebase. Somewhere in the early design, "a project as a lead" and "a project as an accepted, active engagement" had been treated as two separate entities rather than two states of one entity. And every capability we'd built since then — overview screens, scope management, space handling — had been dutifully rebuilt twice, once for each "door," because nothing in the domain model forced the two to stay in sync.
Why is this easy to miss
The insidious part of this kind of problem is that it doesn't look like a modeling problem when it shows up. It looks like a missing feature. It looks like an inconsistent ID. It looks like a duplicate row in a list. Each individual symptom has a perfectly reasonable, perfectly local explanation, and each one can be "fixed" without ever touching the actual cause.
That's exactly what happened here. A teammate discovered that one side of the workflow simply didn't have an overview endpoint that the other side had. Fixable — just build the missing endpoint, right? Another teammate found that the frontend was passing one kind of identifier while the backend expected another. Fixable — just add a mapping layer. A third found that grouping logic was producing duplicate entries because two different code paths were both resolving to the same underlying record, but neither path knew the other existed.
Three different people, three different corners of the system, three "small" bugs. It was only in comparing them side by side in a single conversation that the shape of the actual problem became visible: none of these were independent defects. They were all downstream symptoms of the same upstream decision — that two things which are actually one thing had been split apart at the model level, early enough that the split had been invisible ever since.
Why this isn't the bug you think it is
It's tempting, when you hear "duplicated logic" or "entities out of sync," to reach for a familiar diagnosis: anemic domain model. That's the classic complaint — entities that are just bags of fields, with all the real behavior and business rules smuggled off into service classes, so nothing actually enforces the rules of the domain anywhere central.
But that's not quite what's happening here, and I think it's worth being precise about the difference, because the fix — if we were to fix it — would look very different depending on which diagnosis is right.
An anemic domain model is a behavioral problem: the entity exists, it's singular, but it doesn't know how to do anything. Logic lives elsewhere.
What we had here is a structural problem: the entity itself had been forked. There wasn't one Project with rules about what it could and couldn't do depending on its current state — there were two independent concepts, Project and Lead, each with its own identity, its own endpoints, its own life. They represented the same thing to a human reading the product spec, but the system had no shared vocabulary that captured that. In domain-driven design terms, this is a breakdown of the ubiquitous language and the aggregate boundary — the model drew a line where the business didn't actually have one.
That distinction matters because "make the entities richer" (the usual anemic-model fix) wouldn't have touched this bug at all. You can give an entity all the behavior in the world, but if there are two of them pretending to be one, richness just gets duplicated along with everything else.
The real cost of a forked concept
What makes this class of bug expensive isn't any single instance of it — it's the compounding. Every new feature request that touches "the project" now carries an invisible tax: did we remember to build this for both sides of the fork? Most of the time, someone does remember, because the team is careful. But "most of the time" is exactly the kind of guarantee that produces the bug we started with — the one side that got missed, quietly, until a QA pass or a curious teammate stumbles on it.
There's also a subtler cost: the fork erodes confidence in the system's shape. Once you know that "the thing labeled Lead" and "the thing labeled Project" are secretly the same business concept wearing two different backend costumes, you start to wonder what else in the domain has this property. Every future conversation about "where does this belong" gets a little more expensive, because the map you're using might not match the territory.
The bigger lesson
The part of this I keep coming back to is how ordinary the individual symptoms felt. A missing endpoint. A wrong ID is being passed. A duplicate in a list. None of those, on their own, would have made me suspect a modeling issue — they'd have made me suspect an oversight, a typo, a forgotten migration.
It was only the act of putting three unrelated-looking bugs on the same table and asking "what would have to be true for all three of these to exist at once?" that revealed the actual shape of the problem. That's the habit I'd want any engineer to build: when you find yourself explaining away a second "unrelated" bug with the same kind of hand-wave you used for the first, stop and ask whether they're actually the same bug wearing different clothes.
Sometimes a door really is just a door with a broken hinge. But sometimes it's two doors that were never supposed to be two doors in the first place — and no amount of hinge repair is going to fix that.
Structural fork
As a senior software engineer, the technical name for the problem described in the sources is a structural fork in the domain model.
In more formal architectural and Domain-Driven Design (DDD) terms, this is identified as a breakdown of the ubiquitous language and the aggregate boundary.
Here is a deeper breakdown of how a senior engineer would categorize this problem based on the sources:
- Structural vs. Behavioral Issue: This is not an "anemic domain model" (which is a behavioral problem where logic is missing from entities). Instead, it is a structural problem where the entity itself has been split into two independent concepts that should be one.
- Mismatched Abstraction: It represents a failure to align the codebase with the business reality. The model has "drawn a line where the business didn't actually have one," leading to parallel infrastructure for the same real-world object.
- Identity Dissonance: Because "two things which are actually one thing had been split apart at the model level," the system lacks a shared vocabulary or a single source of truth, causing downstream symptoms like inconsistent IDs and duplicate entries.
- The "Invisible Tax": From a leadership perspective, this is often viewed as a compounding maintenance debt. Every new feature requires a "tax" because developers must remember to implement changes across both sides of the fork to keep them in sync.
In short, while a junior developer might see three separate bugs (a missing endpoint, a wrong ID, and a duplicate row), a senior engineer identifies them as symptoms of a forked domain concept.
Conclusion
In summary, structural forks in a domain model are more than just a case of duplicated logic; they represent a fundamental disconnect between the system's "blueprint" and the business reality it serves. While it is tempting to treat symptoms like missing endpoints, inconsistent IDs, or duplicate entries as isolated, "five-minute" fixes, doing so ignores the underlying "invisible tax" these forks impose on every future development effort.
The real challenge for engineering teams is not just writing richer entities, but ensuring that the ubiquitous language and aggregate boundaries of the domain accurately reflect a single source of truth. When we find ourselves repeatedly explaining away "unrelated" bugs that share a common theme, we must look at the "shape" of these problems collectively to see if our model has quietly split one door into two.
Ultimately, building a resilient system requires us to look beyond simple "hinge repair" and address the foundational structural flaws that erode confidence in our codebase. By ensuring our domain model captures the actual boundaries of the business, we can prevent the system from quietly forking itself into a maintenance nightmare.