AntiPatterns Never Left, We Just Stopped Calling Them by Name

I specialize in developing object-oriented java applications that aligns with business objectives, using Domain-Driven Design principles to ensure technical decisions drive tangible value. By focussing on a deep understanding of the business domain, I craft solutions that solve real problems while maximizing ROI. My approach evaluates the cost/profit ratio of every decision—only implementing technologies when the benefits outweigh the costs. I’ve been called in to revive stalled projects and address challenges where others have struggled. My focus is on creating software that not only meets but exceeds business expectations. Whether working with legacy systems or modern frameworks, I select the right technologies to maximize value—not just follow trends. I believe software should be a strategic asset, and this mindset guides every decision I make in development.
In 1998, a book called AntiPatterns did something unusual: instead of cataloguing good solutions to recurring problems, it catalogued bad ones — the recognizable, recurring ways software projects go wrong. The Blob, Spaghetti Code, Stovepipe Enterprise, Mushroom Management. Each one came with a name, a description of the symptom, and a refactored path out.
Patterns and AntiPatterns are two sides of the same coin. A pattern says: here's a known problem, and here's a solution that tends to work. An AntiPattern is not simply "a bad solution" — it's a description of a recurring failure mode, or of something that actively blocks or resists effective development, even when (especially when) it doesn't look like a mistake at the time.
What makes an AntiPattern dangerous isn't that it's obviously wrong. It's that the failure mode it describes tends to be invisible while it's happening. This is the unfalsifiability problem: if a system works, meaning it runs, it ships and does what it should do, the choice that produced it gets read as validated. The counterfactual (what if we'd done it differently?) is invisible. Nobody runs that experiment. So the failure mode doesn't get diagnosed; it gets repeated, often by other teams, often with conviction, often dressed up as best practice.
Patterns have an entire consulting industry built around teaching them. AntiPatterns, as far as we can tell, mostly don't — there's no equivalent industry whose job is to walk into a project and say "this is Stovepipe Enterprise, and here's what it'll cost you in three years." So the old catalogue — genuinely old now, pre-dating microservices, Kubernetes, Spring Boot, Scrum-as-religion, and the entire modern cloud-native stack — quietly fell out of view. Not because the failure modes it described went away, but because nobody was selling the diagnosis.
Going back to that old catalogue, the question is simple: which of these still apply, and to what, today? The answer, overwhelmingly, was: almost all of them, just wearing different clothes. What follows is sourced from the originals, regrouped into four themes, each of which is really just a different altitude at which the unfalsifiability problem operates — from the codebase, to the organization, to the industry at large.
Chapter 1: Technical Axis vs. Domain Axis
The shape of unfalsifiability here: a working system hides which axis its structure is organized around — until the domain changes, and you discover the boundaries were drawn for the compiler's convenience, not the business's.
Software has at least two legitimate ways to be sliced. One is by what the business cares about — a Risk Summary, a Customer Account, an Order. The other is by technical concern — repositories, services, controllers, DTOs, event handlers. Both are real. The trouble starts when the technical axis becomes the organizing principle and the domain concepts get fragmented across it, because nobody's job is to keep the domain concept coherent anymore — everybody's job is to keep their layer coherent.
Jumble → "Accidental Layering"
The original Jumble AntiPattern describes what happens when horizontal layers (presentation, business logic, data access) and vertical domain slices get intermixed without discipline, producing an architecture that's neither cleanly layered nor cleanly domain-partitioned.
The modern, much more common version of this is subtler and looks like good practice: "put all your queries in a repository layer." On the surface this is just separation of concerns. In practice, it means a concept like "risk total for this category" — which is meaningful only in the context of a Risk Summary — gets implemented as a generic, context-free query method sitting in a repository, available to be called from anywhere, by anything, with no memory of what it's for.
The usual defense is reuse: "if the query lives in the repository, every part of the system that needs a risk total can call the same method." This sounds reasonable until you notice what's actually being reused. You never reuse a query — you reuse what it represents. A query is just an implementation detail; "risk total for this category" is the concept that needs to stay consistent. Reusing the query method gives you textual reuse of some SQL. Reusing the Risk Summary object — calling its riskTotal() — gives you reuse of the context: the rules about what counts, what's excluded, how categories nest, all of it living in one place that knows what "risk total" means.
The failure mode this produces is depressingly specific and common: need A comes along, and the existing query in the repository is almost right but not quite — so rather than fix the shared query (and risk breaking need B, which also calls it), whoever's implementing A copies the query and tweaks it to fit. Now there are two queries called "risk total," subtly different, and nothing in the codebase says they're supposed to mean the same thing — or that they don't anymore.
The opposite also happens, and it's arguably worse. Need A is slightly different from need B, but whoever's working on A assumes they're the same — same name, same shape, looks like the same query — and edits the shared one in place to fit A's requirements. The unit test for B never anticipated this scenario, because nobody writing it imagined "someone will later assume this is also A's query and change it accordingly." So B doesn't break across the board; it breaks in some scenarios — the ones where A's and B's actual requirements diverge — which is exactly the kind of bug that surfaces in production, intermittently, long after the change, and gets debugged as "weird edge case" rather than traced back to a shared query that two different concepts were silently sharing.
Both directions — forking a query that should've stayed shared, and editing a shared query that should've stayed forked — have the same root cause: there's no explicit object whose job it is to own the concept and represent the boundary between what A needs and what B needs. A unit test won't catch either, and this is where it loops back to unfalsifiability directly: a test only encodes what was known at the time it was written. The missing context — that A and B are both expressions of the same domain concept, and a change to one is a change to the meaning of the other, or that they aren't and a change to one must not touch the other — is exactly the thing a context-free query can't carry and a test can't recover after the fact. Keep the query on the Risk Summary, as a method on the aggregate that owns the concept, and both classes of bug become structurally harder to write — not because anyone's more careful, but because there's only one place "risk total" can live, and changing it visibly changes everything that depends on it.
This is accidental layering: structure that exists to organize the technology — how do we talk to the database — at the cost of fragmenting the domain concepts that the technology is supposed to be serving. The repository version works. It compiles, it returns data, the tests pass. The cost only shows up later, when "risk total" quietly stops meaning one thing.
Functional Decomposition + Poltergeists → The Anemic Domain Model
The original Functional Decomposition AntiPattern describes experienced procedural developers writing object-oriented code that's secretly still procedural — classes exist, but they're really just namespaces for functions, operating on data that lives elsewhere. Poltergeists, in the same vein, are short-lived classes whose only job is to kick off a process for some other object and then disappear.
Put these two together and you get a near-perfect description of the "fat service, thin object" shape that shows up across most layered architectures, regardless of language or framework: Service classes full of methods that orchestrate behavior, operating on Entity objects and DTOs that are really just data bags with getters and setters. The class structure is object-oriented. The behavior is procedural — it's Pascal with annotations, or Pascal with decorators, or Pascal with whatever the local ceremony happens to be. The "objects" don't do anything; the services do everything to the objects.
Layered on top of this, the Poltergeists are everywhere: mapper classes that convert entities to DTOs and back, one-shot orchestrator classes, *Factory and *Builder and *Handler classes whose entire lifecycle is "get instantiated, shuttle control from the controller to the service to the repository, disappear." None of these classes know anything. They just move data and call the next thing.
This is the architecture that "put the logic in the service, keep the data in the DTO" produces by default — not because any particular framework forces it, but because it's the path of least resistance once logic and data have been separated by convention, and the path of least resistance is what most codebases end up looking like at scale. The object-oriented vocabulary (classes, methods, "services") is all there. What's missing is anything that resembles an object in the original sense — something that owns both its data and the rules about what that data means, the way the Risk Summary above owns riskTotal().
Lava Flow → Dead Artifacts in Event-Driven Architecture
The original Lava Flow AntiPattern is about dead code and forgotten design decisions that get frozen into an ever-changing codebase — like hardened rock in a lava field, nobody quite remembers how it got there, and nobody's confident enough to remove it.
Event-driven architecture and CQRS can become extremely effective Lava Flow generators. The mechanism is specific: when a state change happens, it gets translated into an event, published, and then handled — possibly by several consumers, possibly asynchronously, possibly with retries, possibly written to an outbox first. Each of those steps is a place where a "rock" can harden: a handler that nobody triggers anymore because the upstream condition that used to fire it was refactored away, a published event type that three downstream services still subscribe to "just in case," a saga step that's technically unreachable but nobody's sure enough to delete it.
The original Lava Flow's solution was a configuration management process that actively hunts down and eliminates dead code. The EDA version of dead code is much harder to hunt, because it isn't sitting in one file you can search for — it's a subscription, a topic, a handler registration, possibly in a different repository than the thing that used to trigger it. The debris isn't dead code in the traditional sense; it's dead connections — and because messaging is fundamentally fire-and-forget, those dead connections don't even fail loudly. A handler nobody needs anymore doesn't throw; it just keeps running, on schedule, consuming compute and network for events that no longer mean anything to anyone. In a monolith, dead code is at least inert — it sits there, unused, until someone deletes it. In EDA, dead code can be active: a ghost process, still executing, still costing money, with no stack trace and no error to tell you it's a ghost.
But there's a cost that shows up even while everything is alive and healthy, which is arguably the more important one: the dependency itself becomes invisible. When A's state change causes B's state change in the same transaction, that causality is right there in the code — a call, a method, something you can read and step through. When A publishes an event and B (eventually, somewhere) handles it, that same causality still exists — B still depends on A having happened — but it no longer exists anywhere in the code. It exists only as a runtime fact: a subscription, a topic name, a piece of configuration. Debugging "why did B happen" stops being a matter of reading code and becomes a matter of reconstructing a causal chain after the fact, across services, via logs, correlation IDs, and timestamps.
That reconstruction can be done — distributed tracing, log aggregation, and correlation IDs all exist precisely to make it possible — but it's worth being honest about what those tools are: compensating machinery, built to recover something a single transaction would have given you for free. A lot of what gets called "modern observability" is, functionally, the cost of paying back the contextualization that decoupling spent. Keeping things that should happen together actually together — same domain object, same transaction, even across multiple methods — doesn't require any of that machinery, and is a lot less likely to generate a Lava Flow in the first place, because there's nothing to subscribe to, lose track of, or reconstruct.
It's also worth separating two things that get conflated under "we need EDA/microservices to scale": scaling and splitting are not the same operation. A monolith can scale horizontally — more instances behind a load balancer — without anything being split apart at all. Splitting a system into services that communicate via events solves a coordination problem (independent deployability, team ownership, different parts needing different resource shapes) — it doesn't, by itself, make anything handle more load. When "we need to scale" is used to justify "therefore we need to split," a capacity problem is being answered with an architecture decision that's actually about organizational boundaries — which may be the right call, but is a different call, justified by different reasons, with the debugging and Lava Flow costs described above as part of its price.
When a state change happens close to the domain core — same object, same transaction, even if not the same method — it's visible. When it happens by firing an event across a transactional boundary, you've traded visibility for decoupling, and the Lava Flow is the price of that trade, paid later, by someone else, in a form that doesn't even announce itself as a cost.
Vendor Lock-In → Framework Lock-In Without a Vendor
The original Vendor Lock-In AntiPattern describes systems that become highly dependent on a proprietary architecture, to the point where switching away becomes prohibitively expensive — historically, think IBM mainframes, or any single-vendor enterprise stack.
The interesting modern twist is that lock-in no longer requires a vendor in the old sense — and then, almost as if to prove the original AntiPattern's point all over again, the vendor relationship quietly grows back. Spring is open source, but the company behind it now sells exactly the kind of commercial support arrangement Vendor Lock-In originally warned about: OSS minor releases get a guaranteed support window of just over a year, after which the application keeps running on the last published artifact, but any newly discovered vulnerabilities have no upstream fix — you're on your own unless you pay for extended coverage.
So the choice, every year or so, per major dependency line, is: pay for enterprise support, or pay in engineering time to upgrade. And "pay in engineering time" is real money with a real number attached — a couple of contractors spending a chunk of their month on framework version bumps, dependency conflict resolution, and re-testing everything that touches the upgraded pieces, adds up to a bill that's directly comparable to a support contract, except it's hidden inside "maintenance" rather than itemized as "vendor cost." Either way, you're paying someone to keep the substrate underneath you current — which is the textbook definition of dependency, just relabeled.
On top of that: a sufficiently "Spring-native" codebase — laced with @Autowired, @Transactional, @Service, component scanning, and the conventions that make all of that work — is enormously expensive to extract from regardless of who you're paying. Not because anyone's charging you to leave, but because your domain logic and the framework's lifecycle have become structurally entangled. The framework isn't a dependency you call; it's the substrate your code lives inside.
This matters because none of it feels like the lock-in the original AntiPattern described. There's a contract now — but it's framed as "support," not as the price of staying put. It feels like "just using a popular, well-supported framework, with optional extras" — which is exactly what makes it durable. The unfalsifiability problem here is almost total: there is no single event that tells you "you are now locked in." You just slowly become unable to imagine the alternative, the renewal invoice (or the upgrade sprint) arrives on schedule, and the system keeps working — so the question of whether this is actually cheaper than the alternative never gets asked, let alone answered.
Chapter 2: Adoption Without Evaluation
The shape of unfalsifiability here: at the industry scale, popularity itself becomes the evidence. The road not taken is invisible, so "widely adopted" quietly substitutes for "evaluated and found correct for this context."
Continuous Obsolescence → Dialect Drift Inside "the Same Language"
The original Continuous Obsolescence AntiPattern is about ecosystem churn: technology moves fast enough that finding compatible versions of things that actually interoperate becomes its own ongoing project, and developers spend real effort just keeping the floor from shifting under them.
There's a related but distinct failure that the original framing doesn't quite capture, and Scala is the clearest historical example of it. Scala's problem was never really "too many releases" — it was that the language gave every team enough expressive power (implicits, operator overloading, macros, DSL-building features) to define its own dialect. Walking into a new Scala codebase often meant learning that codebase's Scala before you could be productive in it, on top of learning Scala itself. The language was technically one language; in practice it was as many languages as there were teams willing to use its more expressive corners.
Java spent a long time being the opposite of this on purpose — verbose, explicit, "English-like," deliberately leaving little to the imagination, precisely so that a Java codebase from one team looked recognizably like a Java codebase from another. But the last decade of Java releases — lambdas, streams, var, records, sealed types, pattern matching, and the steady cultural push toward "boilerplate reduction" — has been adding exactly the kind of expressive, compact, idiomatic features that Scala had from day one. None of these features are bad in isolation. But each one raises the floor of what "reading Java" requires, and — just like Scala — different codebases adopt different subsets of them, idiomatically or not, without anyone deciding this as policy. A codebase built around streams-of-records-with-pattern-matching reads nothing like one that's still mostly loops and getters, even though both compile as "just Java 21." The dialect fragmentation Scala had in the open, Java is quietly acquiring feature-by-feature, each addition individually justified as "less boilerplate," with nobody tracking the cumulative effect on how many distinct styles of Java a developer now needs to be fluent in before "knowing Java" actually means being productive.
This is Continuous Obsolescence at the level of readability rather than dependency versions — the floor for entry-level legibility keeps rising, a release at a time, and because each individual feature is small and well-intentioned, there's never a single moment where anyone evaluates whether the codebase as a whole still meets its own bar for "anyone on the team can read this."
Golden Hammer
The original Golden Hammer is the most literal of the bunch and barely needs updating: a familiar technology or concept, applied obsessively to problems it doesn't fit, because it's the tool the team knows. The original's prescribed fix — expand developers' knowledge through education, training, and book study groups, so they have alternatives to reach for — is, charmingly, still the prescribed fix in 2026, and still mostly doesn't happen.
What's changed is the scale of the hammer. In 1998 a Golden Hammer might be one design pattern, applied everywhere. Today it's an entire platform — Kubernetes for a five-person team's internal tool, Kafka because the last company used Kafka, a service mesh for an application with three services. The hammer got bigger, but the mechanism — familiarity substituting for fit — is identical.
Continuous Obsolescence and Golden Hammer are, in a sense, mirror images. Golden Hammer is under-using a toolkit's diversity — one familiar tool, applied everywhere, regardless of fit. Dialect drift is over-diversifying a language's feature usage until the codebase itself becomes a toolkit nobody fully knows — every corner adopted because it was available and looked like an improvement, with nobody asking whether the codebase, as a whole, was better off with a smaller, more uniform set of idioms. Same lack of deliberateness, opposite direction.
Architecture by Implication → Survivorship Bias as Architecture
The original AntiPattern describes overconfidence carried forward from past successes: a general approach that worked once gets applied to the next system, without anyone checking whether the new system's risks and requirements are actually similar.
The deeper version of this: every system is, in practice, only ever built once. The cheaper, simpler alternative was never actually built, so there's no comparison to make. If the system that was built works, that gets read as success — full stop. Nobody can point to the parallel universe where the team built the boring monolith instead of the microservices, or skipped CQRS, or didn't introduce the event bus, and ask whether that version would have shipped faster, cost less, and been easier to change.
This is why patterns like EDA, CQRS, and microservices — which have entirely legitimate origin contexts (genuinely high scale, genuinely independent teams, genuinely eventual-consistency-tolerant domains) — end up applied far outside those contexts. The pattern worked somewhere, visibly, loudly, in a conference talk. The boring alternative never got a conference talk, because it was boring, because nothing went wrong, because there was nothing to present. "It shipped and the company didn't die" gets read as validation of the pattern, when it's really just validation that the constraints were tolerable — which tells you nothing about whether the pattern was necessary.
Intellectual Violence → Complexity as Social Leverage
The original AntiPattern describes someone who understands a theory, technology, or buzzword using that knowledge to intimidate others in a meeting — winning the argument not on merits, but by making disagreement look like ignorance.
The modern version doesn't even require an intimidator. CQRS, Dependency Injection, Event-Driven Architecture — these are genuinely complex enough that disagreeing with their use requires demonstrating you understand them well enough to critique them specifically. "I don't think we need this" sounds, to a room that's already nodding, indistinguishable from "I don't understand this." So the safer move — for almost everyone in the room — is to nod too. The complexity itself does the intimidating; nobody has to play the difficult one in the room.
This connects directly to the rest of the chapter. Continuous Obsolescence and Golden Hammer explain what gets adopted — features and tools chosen for familiarity or availability rather than fit. Architecture by Implication explains why nobody's checking whether the adoption was the right call (no visible counterfactual). Intellectual Violence explains why, even when someone privately suspects it wasn't the right call, they don't say so out loud. Different mechanisms, same outcome: complexity that nobody individually chose, but everyone collectively rubber-stamped.
Chapter 3: The Cure Regrows the Disease
The shape of unfalsifiability here: at the organizational scale, a structural fix is judged purely by whether the system still works afterward — not by whether the underlying disease actually left, or just moved to an organ nobody's looking at.
Spaghetti Code → Distributed Spaghetti
The original Spaghetti Code AntiPattern is the classic: ad hoc structure, no clear flow, difficult to extend or optimize, fixable mainly through disciplined, ongoing refactoring.
Microservices are frequently sold as the cure for this — break the tangled monolith into small, independent, individually-comprehensible services. And at the scale of a single service, that's often true: a small service can be spaghetti-free in a way a 100-object monolith struggles to be.
But the failure mode doesn't require any single service to be tangled. It requires the system to be tangled — and distributing the tangle across network boundaries doesn't untangle it, it just makes each individual strand harder to see and far more expensive to follow. Spaghetti Code was always survivable, in part, because the compiler caught some of it — a method signature change that breaks twelve callers is a build failure, immediately, locally, before anything ships. Distributed spaghetti loses that safety net entirely: the equivalent change is a contract change between services, the breakage is a runtime error in production, possibly in a service owned by a different team, possibly days later.
The decomposition didn't remove the failure mode. It changed its blast radius — and traded a problem you could see (a big tangled codebase, sitting right there, clearly someone's problem) for one you mostly can't (a tangle of contracts and assumptions spread across services and teams, nobody's full-time job to track).
Stovepipe Enterprise → Microservices and the Ossified Boundary
The original Stovepipe Enterprise describes a lack of coordination and planning across systems — each one solving its own slice in isolation, duplicating effort, creating integration headaches with everyone else.
Microservices done at the boundary level produce something that looks like the opposite problem but shares the same root cause. The boundaries get drawn carefully, thoughtfully, with real coordination — at one point in time, based on the team's current understanding of how the business works. And then the business — its processes, its terminology, its rules about what belongs together — keeps changing, because that's what businesses do. The code structure ossifies around a snapshot of domain understanding that the domain itself has already moved past.
The original Stovepipe Enterprise is mostly about waste and duplication from never coordinating. The microservices version is almost the inverse symptom from the same underlying mistake: treating the system as a sum of independently-evolvable parts, when the thing that actually needs to evolve — the shared understanding of the domain — doesn't respect the part boundaries at all. It's easy to split a system along today's understanding. It's extremely hard to un-split it when that understanding changes, which is exactly when you'd need to.
None of this means microservices are categorically wrong. It means microservices trade a maintenance cost you can see — a large, tangled codebase, sitting there, undeniably someone's problem — for one that's much harder to see: accidental complexity that didn't go away, it moved to the seams between services and got a network hop attached to it.
Throw It Over the Wall → Platform Teams
The original AntiPattern describes object-oriented guidelines and implementation plans — meant as flexible suggestions — getting treated as rigid mandates by the time they reach downstream developers, accumulating false authority as they pass through approval processes.
The modern instance is almost a perfect mirror, but at the team level rather than the document level. DevOps, as a movement, was explicitly framed as the antidote to exactly this kind of wall-throwing: "you build it, you run it" — collapse the separation between the people who write software and the people who operate it, so nobody can throw anything over a wall because there's no wall.
What actually happened, often, is that the tooling required to "build it and run it" — CI/CD pipelines, Kubernetes, observability stacks — became sophisticated enough to need its own dedicated team. That team chooses the platform (often the popular thing, see Chapter 2), builds the pipelines, and now sits between product teams and production — a new wall, one level removed, staffed by people who weren't there when the original wall was being torn down and likely don't think of themselves as a wall at all.
This is the clearest example of a pattern that recurs across this entire article: the antidote regrows the disease in a different organ. Mushroom Management (Chapter 4) gets "solved" by Scrum's Product Owner role, which recreates the intermediary. Throw It Over the Wall gets "solved" by DevOps, which recreates the department. In both cases, the system afterward works — which is exactly why nobody notices the disease came back. It just moved.
Chapter 4: Who Feels the Pain Doesn't Decide
The shape of unfalsifiability here: at the individual/role scale, the person making a structural decision is organizationally insulated from its consequences — so they never receive the feedback signal that would tell them the decision was wrong.
Mushroom Management → Scrum's Intermediary, Reborn
The original Mushroom Management AntiPattern describes a deliberate policy of keeping developers isolated from end users — requirements arrive second-hand, filtered through architects, managers, or analysts, who stand between the people building the thing and the people who'll use it.
Modern Scrum was, in part, supposed to fix this — user stories as conversations, the whole point being a direct, ongoing dialogue between the people who need something and the people building it, with the story as a prompt for discussion rather than a finished spec.
In practice, the story very often becomes the instruction rather than the conversation-starter, and the Product Owner becomes the very intermediary the process was meant to remove — now institutionalized as a defined role, with its own ceremonies, sitting precisely where the "mushroom" used to sit. It's almost recursive: an AntiPattern from 1998 describing a problem, and a 2001-era process explicitly designed to address it, regrowing the same shape inside the cure.
Design by Committee + Grand Old Duke of York → Chickens, Pigs, and Flattened Roles
The original Design by Committee AntiPattern is the classic standards-body failure: overly complex architecture, lacking coherence, because too many people with too little shared context (and too little personal stake) are making decisions by committee. Grand Old Duke of York, separately, observes that programming skill doesn't equate to skill in defining abstractions — there are, in practice, two different skill sets (call them abstractionists and implementationists), and ignoring that distinction hurts projects.
The "chickens and pigs" framing from agile folklore captures both at once: chickens have opinions about the farm but don't lay the eggs; pigs provide the bacon and feel the consequences. Design by Committee is chickens designing for pigs. Grand Old Duke of York, reframed through modern Scrum, is something slightly different and arguably worse: Scrum often flattens the abstractionist/implementationist distinction entirely — in principle, anyone on the team can take the architectural decision for a given sprint, regardless of whether they have the abstraction-defining skill the original AntiPattern says is rare and distinct.
The 1998 framing at least acknowledged the skill gap and tried to address it through process — get the right people defining abstractions. The flattened-role version sometimes pretends the gap doesn't exist at all. Both versions, old and new, share the same underlying mechanism with Mushroom Management and Throw It Over the Wall: the people who'll live with the architectural decision, day to day, are not reliably the people making it — and the system working afterward doesn't tell you whether it was the right decision, only that it wasn't an immediately fatal one.
Corncob
The original AntiPattern is blunt: a Corncob is a difficult person who obstructs and diverts the development process, typically dealt with through tactical, operational, or strategic organizational maneuvering rather than direct confrontation.
What's worth naming explicitly is why this works as well as it does, and for as long as it does. A Corncob's obstruction is rarely framed as obstruction — it's framed as caution, rigor, "just asking questions," or insisting on a process step that conveniently never quite finishes. None of that is free: every round of relitigating a decision, every extra review gate, every "let's circle back" has a cost, paid by the people waiting on the decision. But that cost lands on other people's timelines, not the Corncob's — which means the Corncob never receives the feedback signal that would tell them the obstruction has a price. It's the same mechanism as Design by Committee and Mushroom Management, just personalized: the person creating the friction is structurally insulated from feeling it, so the friction has no reason to stop.
Closing: Reinventing the Wheel, On Purpose
There's one more entry from the original list worth ending on, because it inverts the usual direction of travel: Reinvent the Wheel. In 1998, this was unambiguously a problem — a pervasive lack of technology transfer between projects meant teams kept rebuilding things that already existed elsewhere, at real cost in time, money, and risk.
In 2026, with the sheer density of convenience frameworks available — frameworks that, as several of the chapters above describe, tend to arrive with their own lifecycle, their own conventions, their own lock-in, and their own accidental layering — "reinventing the wheel" sometimes means writing fifty lines of code that do exactly what you need, instead of pulling in a dependency that does that and a hundred things you don't, each of which is now a thing your codebase is entangled with.
There's a second, less obvious payoff. A wheel you build yourself — a rich domain model, built from first principles around your actual concepts (Risk Summaries and all the rest) rather than around a framework's idioms — tends to survive. It can be carried forward across framework versions, across migrations, sometimes across entire platform changes, because it was never coupled to any of those things in the first place. The framework-shaped wheel, by contrast, often has to be substantially rebuilt with every major version bump, every "the framework now does this differently" release — which is the upgrade-treadmill cost from Chapter 1's Vendor Lock-In section, paid again and again. "Reinvent the wheel, once, properly" can be cheaper over a decade than "rent someone else's wheel, and rebuild your dependence on it every year or two."
This isn't a blanket argument against frameworks, any more than the rest of this article is a blanket argument against microservices, CQRS, or Scrum. It's the same observation, one more time, from a different angle: every one of these "cures" was a legitimate answer to a real problem, in some context. The AntiPatterns above aren't lists of things to never do. They're the original, largely-forgotten warning labels — written before any of today's specific technologies existed, describing the shapes of failure with enough precision that, almost thirty years later, you can hold the old description up against today's stack and watch it line up, name for name, almost too well.
Nobody's selling the antidote. But the list was always right there.





