Chapter 9: Anti Patterns
Anti patterns are recurring solutions that look convenient at first glance, then quietly multiply trouble. This chapter names common traps, shows how to spot them, and offers practical ways to refactor toward cleaner designs. Each section includes symptoms, causes, a brief example, and remedies.
The God Object
A God Object is a class or module that knows too much and does too much. It centralizes state and behavior that should belong to several collaborators. The result is fragile code that resists change and spreads hidden coupling across a codebase.
Symptoms of GodObject
Look for very large files, hundreds of methods, long switch statements, and fields that reference many unrelated concerns. Small changes ripple through the class, unit tests are slow to write, and developers avoid touching it without a buddy present.
Why teams create a GodObject
Early speed and unclear boundaries push teams toward a single hub of logic. Deadlines, lack of domain modeling, and fears about creating too many files all feed the growth of this class.
Example of a GodObject
In this example a single class performs configuration loading, payment processing, email notifications, and logging. The placeholders use … to indicate omitted content.
class SystemManager:
def __init__(self, cfg_path):
self.cfg = self._load_cfg(cfg_path)
self.db = self._connect_db(self.cfg["dsn"])
self.logger = Logger(self.cfg["log"])
def _load_cfg(self, path): …
def _connect_db(self, dsn): …
def process_payment(self, user_id, amount): …
def send_receipt(self, user_id): …
def rebuild_indexes(self): …
def rotate_logs(self): …
Refactoring strategies for GodObject
Extract cohesive services, create clear boundaries, and inject dependencies. Start with a narrow slice; pull one responsibility into its own class, write tests, then repeat. Consider patterns such as Facade for integration points and Strategy for swappable behavior.
The Blob
The Blob is a data class with fields galore and very little behavior, coupled to a swarm of procedural functions that manipulate it. Logic lives outside the data, which leads to duplication and scatter.
Symptoms of Blob
Wide DTOs with dozens of public attributes, validation code scattered across helper utilities, and a habit of passing the same structure through many layers. The object becomes a suitcase that everyone rummages in.
Why a Blob appears
Teams begin with transport objects to cross boundaries, then never bring the behavior home. Fear of circular dependencies and a thin-domain mindset keep logic outside where it keeps duplicating.
Example of a Blob
Behavior is not attached to data. Everything pokes at the same structure.
class OrderData:
def __init__(self, id, items, discounts, user, address):
self.id = id
self.items = items
self.discounts = discounts
self.user = user
self.address = address
def calc_total(order): …
def apply_discounts(order): …
def validate_order(order): …
Refactoring strategies for Blob
Move behavior to the data that it touches. Introduce rich domain objects with invariants. Replace procedural helpers with methods on domain classes. Use ValueObject types for immutable concepts such as money, quantity, and address.
| Smell | Shift |
| Procedural helper | Method on domain object |
| Many public fields | Encapsulated properties |
| Validation scattered | Invariants in constructors |
Lava Flow
Lava Flow occurs when experimental or obsolete code cools into the codebase and becomes hard to remove. Dead features, half migrations, and commented fossils remain because nobody is sure what depends on them.
Symptoms of LavaFlow
Unreachable branches, TODOs from years ago, duplicated modules with names like new_… and old_…, and fear of deletion. Build scripts reference legacy paths that nobody can safely touch.
Why LavaFlow forms
Prototypes promote into production without cleanup, large rewrites stall, and knowledge leaves with team members. Without tests, deletion feels risky, so code accretes like cooled rock.
Example of LavaFlow
Two competing implementations sit side by side, both wired in subtle ways.
def compute_price(v1_order): … # legacy
def compute_price_v2(order, rules): … # intended replacement
# TODO move callers to compute_price_v2 and delete v1 … someday
Refactoring strategies for LavaFlow
Build a safety net, then remove with confidence. Add characterization tests that record current behavior, route new calls through an adapter toward the replacement, and delete unused entry points once coverage exists.
Spaghetti Code
Spaghetti Code is a tangle of jumps, implicit state, and cross-cutting calls. Flow is hard to follow; control paths weave and loop through layers until the mental stack overflows.
Symptoms of Spaghetti
Functions call deep into unrelated modules, global state changes mid-execution, and bugs vanish when you add print statements. Fixes in one area break distant features without warning.
Why teams cook Spaghetti
Short-term patches, lack of boundaries, and mixing levels of abstraction produce tangles. A missing architecture allows any piece to talk to any other piece at any time.
Example of Spaghetti
Flow jumps between concerns, with globals and callbacks on the same path.
state = {"user": None, "cart": []}
def add_item(id): …
def save_cart(): …
def checkout():
if state["user"] is None:
prompt_login(lambda: checkout())
else:
add_item("bonus")
save_cart()
notify("done") # side effect deep in UI
Refactoring strategies for Spaghetti
Choose a primary flow and separate concerns. Introduce modules for orchestration, state, and side effects. Use Facade to centralize coordination, apply Command to model steps, and inject collaborators to break global state.
Golden Hammer
The Golden Hammer is the habit of solving every problem with the same familiar tool. A team succeeds with a library or pattern, then sees every future challenge as another nail. The result is forced fits and accidental complexity.
Symptoms of GoldenHammer
One framework appears in places that do not suit it, abstractions are layered where a simple function would do, and performance falls because the chosen tool fights the shape of the problem.
Why GoldenHammer happens
Success stories, cargo-cult advice, and hiring pipelines that reinforce one stack lead teams to habitual choices. Tool familiarity feels like safety, so tradeoffs are not weighed properly.
Example of a GoldenHammer
A message queue is used for synchronous request handling because the team prefers its dashboards. The system becomes slower and harder to debug.
def get_profile(user_id):
# Puts a request on a queue, then polls for result … for a read path
enqueue({"type": "FETCH", "id": user_id})
return wait_for_result(user_id, timeout=2.0)
Refactoring strategies for GoldenHammer
Start with the simplest workable design. Write short decision records that list options and tradeoffs, then choose tools that match the problem. Prefer small patterns like Strategy or Policy objects that allow swapping implementations without forcing one global choice.
| Context | Prefer |
| Simple synchronous reads | Direct call with retries |
| High fan-out writes | Asynchronous queue |
| Static rules that evolve | Strategy with small policies |
Comparing Anti Patterns
Although these traps differ, they share a theme. Each hides a boundary that should be explicit. The God Object hides service boundaries, the Blob hides behavior inside helpers, Lava Flow hides dead code under fear, Spaghetti hides control flow, and the Golden Hammer hides choice under habit. Make boundaries visible and most anti patterns begin to melt.