Skip to content

Anti-patterns and Pattern Abuse

The recurring bad solutions — god object, anaemic domain, pattern fever — why each one felt reasonable at the time, and how to reverse it.

Intermediate19 minUpdated 8 August 2026

An anti-pattern is a solution that recurs and makes things worse. The word is often used simply to mean "code I dislike", which is not useful. The useful definition has two parts: it is common enough to have a name, and it seemed reasonable when it was introduced.

That second part is the interesting one. Nobody sets out to build a god object. Understanding why each of these felt right at the time is what stops you from building the next one.

Conceptual diagramtopics have no limitrules have nowhere to liveno variation to absorb ittrust outruns evidencesplit by code, not domainabandon itOrganise by topicGod objectKeep data separate from logicAnaemic domain modelApply the cataloguePattern feverUse the tool that workedGolden hammerSplit the monolithDistributed monolithStart a migrationLava layer
Diagram, as text
  • Organise by topic leads to God object (topics have no limit)
  • Keep data separate from logic leads to Anaemic domain model (rules have nowhere to live)
  • Apply the catalogue leads to Pattern fever (no variation to absorb it)
  • Use the tool that worked leads to Golden hammer (trust outruns evidence)
  • Split the monolith leads to Distributed monolith (split by code, not domain)
  • Start a migration leads to Lava layer (abandon it)

Read the left column as reasonable decisions and the right as what each becomes when it is applied past the evidence that justified it.

God object

What it looks like: one class of thousands of lines, with a dozen collaborators, that every feature must touch. It has the most merge conflicts in the repository.

Why it happened: each addition was reasonable. Placing an order belonged in OrderService; so did cancelling, refunding, re-sending and address changes. The class was organised by topic, and topics have no natural limit.

Reversing it: split by operation rather than by topic, using the dependency list as evidence. A method that uses two of eleven collaborators is a candidate to move; methods that share collaborators belong together. This is measurable rather than a matter of taste, and it gives a review argument that is hard to dispute.

Anaemic domain model

What it looks like: domain classes with only getters and setters; all the behaviour in services that manipulate them from outside.

Why it happened: frameworks reward it — ORMs map plain data, serialisers expect plain data, and the layered tutorial puts logic in a service. It also feels tidy: data here, logic there.

Why it is bad: rules end up wherever the data was needed, so the same rule appears in a controller, a batch job and a report, drifting between them. Nothing prevents an object from being put into an invalid state, because the object has no opinion. This is the fat controller's twin — logic accumulates in the middle because the model offers nowhere for it to live.

Reversing it: move one invariant at a time. order.cancel() that refuses if already shipped, rather than a service checking status and setting a field. Each move is small, testable and independently valuable.

Pattern fever

What it looks like: Strategy, Factory and Observer in a module with one implementation of everything and no subscribers. Class names ending in the pattern they use.

Why it happened: someone learned the catalogue and was looking for places to apply it — a reasonable instinct, misdirected. The code is not wrong; it is unpaid-for.

Reversing it: for each abstraction, name the flexibility it buys and check whether it is used. An interface with one implementation, a factory with one branch, an event with no listeners — each is indirection charged and nothing bought. Inline them one at a time.

Golden hammer

What it looks like: every problem solved with the same tool. Everything is a microservice; everything is an event; everything is a rules engine.

Why it happened: a tool solved a real problem well once, so it earned trust, and trust generalised past its evidence.

Reversing it: ask what would have to be true for a different approach to win. If the answer is "nothing", the tool has stopped being a choice.

Distributed monolith

What it looks like: services split by technology or by table, all deployed together, all calling each other synchronously, none independently releasable. All the operational cost of microservices with none of the autonomy.

Why it happened: the split followed the code's existing structure rather than the domain's boundaries, so what were function calls became network calls without any coupling being removed.

Reversing it: merge services that always change together — merging is legitimate and under-used — and re-split along boundaries where teams and data actually diverge.

Primitive obsession and the stringly-typed domain

What it looks like: amounts as numbers, identifiers as strings, statuses as strings compared against literals.

Why it happened: it is the path of least resistance, and each individual case is genuinely trivial.

Why it costs: validation is repeated everywhere or nowhere, a currency mismatch or a units error is undetectable, and a user id can be passed where an order id was expected because both are strings. Small value types — Money, OrderId, an enum for status — make whole categories of bug unrepresentable.

The lava layer

What it looks like: three generations of approach coexisting — a legacy layer, a half-finished migration to a second, and a newer third — with new code using whichever the author knew.

Why it happened: each migration was started with good intent and abandoned when priorities moved. The pattern is not the layers; it is the abandonment, which is why strangler fig and branch by abstraction both insist on a commitment to finish.

Reversing it: pick one target and finish, with a visible number and a rule stopping new usage of the others. Adding a fourth "clean" approach is the single most common wrong response.

Check your understanding

Common mistakes

Using "anti-pattern" to mean "code I dislike". It devalues the term and loses the argument.

Treating anaemic as always wrong. Data transfer objects and configuration records are legitimately just data.

Responding to a lava layer with a fourth approach. It reproduces the cause.

Splitting a god object by topic into smaller topics. The same failure, distributed.

Arguing about size rather than cohesion. A line limit invites a debate about the number.

Removing all abstraction after diagnosing pattern fever. Some of it is load-bearing; check each one.

Splitting services along the code's structure. That is how a distributed monolith is built.

Practice

You join a team whose codebase has a god object, an anaemic domain, and two abandoned migrations. You have limited political capital. What do you do in the first month, and what do you deliberately leave alone?

Write your own answer first, on paper or in a note. Then compare.

Show a hint

Fixing all three at once fails. Which one, fixed partially, makes the others easier?

Show a strong answer

In the first month, mostly nothing structural. A new joiner proposing three refactors has no evidence and no credibility, and the proposal will be received as criticism of the people who wrote it. The first month is for understanding why the code is the way it is — and some of it will have reasons that are not visible from the outside.

What I would actually do:

  • Ship features through the existing structure, deliberately. This buys the standing to propose changes later, and more importantly it teaches me where the pain actually is. It is frequently not where it appears from a first reading.
  • Collect evidence rather than opinions. Which files change most often, which have the most merge conflicts, where bugs cluster, which tests are slowest. This turns "the god object is bad" into "this file was touched in 60% of last quarter's changes and caused four conflicts a week" — an argument nobody has to take personally.
  • Ask why the two migrations stopped. This is the highest-value question available. If they stopped because priorities moved, a third attempt might work. If they stopped because the target was wrong, or because someone senior objected, a third attempt will stop too — and I would rather know that before proposing one.

What I would attack first, at the end of the month: the anaemic domain, one invariant at a time.

  • It is the cause of the other two. Rules have nowhere to live, so they accumulate in the god object; the migrations were probably attempts to reorganise that accumulation rather than to remove its cause.
  • Each move is tiny and independently valuable. Moving cancel() onto Order with its own validity check is a small diff, obviously correct, and testable without infrastructure. It needs no plan and no permission.
  • It shrinks the god object as a side effect, which is a much easier sell than a refactor proposal — the class gets smaller as a consequence of work that had its own justification.

What I would deliberately leave alone:

  • Both abandoned migrations. Restarting one is a large commitment and I do not yet know why they failed. Adding a third approach is the worst available move.
  • The god object as a target in itself. Attacking it directly is a large, contentious change. Let it shrink from the domain work first, then propose a split with the dependency-usage evidence.
  • Anything with no observable pain. Ugly code that nobody touches and that does not break is not worth political capital.

What I would say out loud early: nothing critical. If asked, "there is a lot here I do not understand yet" is both true and the correct answer at week one.

A team argues that their event-driven architecture is not a distributed monolith because services communicate asynchronously. Devise a test to settle it.

Write your own answer first, on paper or in a note. Then compare.

Show a hint

Asynchrony is not autonomy. What can each team actually do alone?

Show a strong answer

Asynchrony is a communication mechanism; the question is about autonomy. Services can be entirely event-driven and still be unable to change independently, which is exactly what a distributed monolith is. So the test must measure independence, not transport.

The tests I would apply, in order of how decisive they are:

  • Can one service be deployed alone, right now, without coordinating with any other team? If deployments must be sequenced, or a release train exists, they are one system with several processes. This single question settles most cases.
  • Can a service add a field to an event without anyone else's involvement? Additive change should be free. If it requires a coordinated release, the event schema is a shared compile-time contract in disguise.
  • Does a change to one service's data model force changes in others? Events carrying the internal model mean every consumer is coupled to it, and asynchrony has changed nothing about that.
  • Can a service be down for ten minutes without the user-facing system failing? Truly asynchronous consumers catch up. If a downed consumer breaks the product immediately, the interaction is synchronous in everything but name — the event has become a request with a queue in front of it.
  • Are events named as facts or as instructions? OrderPlaced is a fact and its producer does not know who reacts. SendConfirmationEmail on a queue is a remote procedure call with extra steps, and the producer is directing its consumer.

The measurement I would trust over all of these: deployment frequency per service, and how often deployments cluster. If six services always deploy within an hour of each other, they are one unit regardless of what the diagram shows. This is observable from CI history rather than from anyone's opinion, which is why I would lead with it.

How I would present the result: not as "you have a distributed monolith", which is an accusation, but as "here is what we can and cannot do independently today". If the answers are good, the team is right and the discussion ends. If they are not, the specific failing test points at the fix — usually events carrying internal models, or events named as commands — and those are tractable changes rather than a re-architecture.

One thing worth conceding to them: asynchrony genuinely does buy something even without autonomy. Temporal decoupling means a slow consumer does not slow the producer, which is real and valuable. The argument is about whether they have also bought independent deployability, not about whether they have gained nothing.

Key takeaways

  • An anti-pattern is a recurring solution that makes things worse — and each one seemed reasonable when introduced.
  • God object: organised by topic rather than by operation. Split using dependency usage as evidence.
  • Anaemic domain model is the fat controller's twin: no home for rules means rules scatter and drift. Move one invariant at a time.
  • Not everything should be rich. Data transfer objects and configuration records are legitimately just data.
  • Pattern fever is indirection charged with nothing bought — name the flexibility each abstraction buys and check it is used.
  • Distributed monolith: split by code structure rather than by domain boundaries. Merging services is a legitimate fix.
  • Lava layer is caused by abandonment; the correct response is to finish one migration, never to add a fourth approach.
  • The underlying skill is noticing when a good default has outlived the evidence that justified it.

Further reading

  • William Brown, Raphael Malveau, Hays McCormick and Thomas Mowbray, AntiPatterns (Wiley, 1998) — the book that named most of these.
  • Martin Fowler, "AnemicDomainModel" (2003).
  • Brian Foote and Joseph Yoder, "Big Ball of Mud" (Pattern Languages of Program Design 4, 1999).
  • Martin Fowler, Refactoring, second edition (Addison-Wesley, 2018) — code smells and their remedies.
  • Sam Newman, Building Microservices, second edition (O'Reilly, 2021) — the distributed monolith.
  • Mike Hadlow, "The Lava Layer Anti-Pattern" (2014).
  • Eric Evans, Domain-Driven Design (Addison-Wesley, 2003).

Check yourself

3 questions in this lesson. Answer each once to complete it.

Practice this

Next lessonRefactoring to Patterns
Lessons in this track