<< Home

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.

💡 When carving up a large class, follow the rule of one reason to change. If two methods change for different reasons, they belong in different places.

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.

⚠️ Characterization tests confirm current behavior, not ideal behavior. Record what the system actually does before changing it.

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.

💡 If a function changes more than one kind of thing, split it so each function performs a single, clear step that can be tested in isolation.

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.