All chapters in this track
- 01Chapter 1: Why Object Orientation Exists
- 02Chapter 2: Classes, Objects, and Instances, Precisely Defined
- 03Chapter 3: The Four Pillars I: Encapsulation
- 04Chapter 4: The Four Pillars II: Abstraction
- 05Chapter 5: The Four Pillars III: Inheritance
- 06Chapter 6: The Four Pillars IV: Polymorphism
- 07Chapter 7: Composition vs. Inheritance
- 08Chapter 8: Interfaces and Abstract Classes: When to Use Which
- 09Chapter 9: SOLID, Part 1: Single Responsibility & Open/Closed
- 10Chapter 10: SOLID, Part 2: Liskov Substitution, Interface Segregation, Dependency Inversion
- 11Chapter 11: Basic UML for Communicating Design
- 12Chapter 12: Anti-Patterns: God Objects, Tight Coupling, Deep Hierarchies, the Fragile Base Class, Anemic Domain Models
- 13Chapter 13: OOP Idioms Across TypeScript and Python — and Where OOP Is the Wrong Tool
- 14Chapter 14: When OOP Breaks Down: Real-World Failure Modes
- 15Chapter 15: Design Exercise: Parking Lot System
- 16Chapter 16: Design Exercise: Library Management System
- 17Chapter 17: Design Exercise: Elevator Controller
- 18Chapter 18: OOP in Interviews: What a Strong Answer Looks Like
Chapter 12 of 18
Chapter 12: Anti-Patterns: God Objects, Tight Coupling, Deep Hierarchies, the Fragile Base Class, Anemic Domain Models
Why this matters
Every principle in this book has a corresponding failure mode when ignored, and those failure modes have names because they recur constantly across unrelated codebases and teams. Recognizing them by name lets you diagnose a struggling codebase quickly and explain precisely what's wrong in a code review, instead of a vague "this feels messy."
God Object
[BEGINNER] A single class that knows about and controls far too much of the system — user management, billing, notifications, reporting, all in one ApplicationManager class. It's the direct violation of Single Responsibility (Chapter 9) taken to its extreme.
// A god object: every unrelated concern routed through one class.
class ApplicationManager {
createUser(data: object): void { /* ... */ }
processPayment(amount: number): void { /* ... */ }
sendNotification(message: string): void { /* ... */ }
generateReport(): string { /* ... */ return ""; }
validateInput(data: object): boolean { /* ... */ return true; }
// ...growing indefinitely, because it's the easiest place to add "just one more thing"
}
[IN PRACTICE] God objects rarely start that way — they accrete. Each individual addition ("just route this one thing through the existing manager, it's already wired up everywhere") seems reasonable in isolation; the failure is cumulative, which is why catching it requires periodically stepping back and asking "does this class still have one coherent responsibility?" rather than judging each individual PR alone.
Fix: apply SRP — split by responsibility into UserService, PaymentService, NotificationService, ReportService, each independently testable and changeable.
Tight Coupling
[BEGINNER] Two classes are tightly coupled when a change to one routinely forces a change to the other, because one depends directly on the other's concrete implementation rather than on an abstraction.
class EmailSender {
send(to: string, body: string): void { console.log(`Emailing ${to}`); }
}
class SignupService {
private emailSender = new EmailSender(); // hard-wired — the exact violation DIP (Chapter 10) fixes
signup(email: string): void {
// ...
this.emailSender.send(email, "Welcome!");
}
}
If SignupService needs to support SMS confirmation for some users, or needs to be tested without actually sending email, the hard-wired dependency has to be untangled first. This is the same failure DIP addresses — introducing an interface (Notifier, as in Chapter 7) and injecting the implementation decouples the two.
Deep Inheritance Hierarchies
[BEGINNER] Covered in Chapter 5 as inheritance's central cost: each additional level of a hierarchy increases the blast radius of any change, and understanding a leaf class requires understanding its entire ancestor chain.
LegalEntity → Person → Employee → Manager → SeniorManager → Director
A change anywhere in this chain can silently affect every class below it. Worse, a field or method added at Employee for a Manager-specific need (violating SRP at the hierarchy level) can end up inherited by classes where it makes no sense — a classic sign the hierarchy was built around convenient code reuse rather than genuine substitutable is-a relationships (Chapter 7's central lesson).
Fix: flatten the hierarchy; extract shared, cross-cutting behaviors into composed collaborators instead of deeper inheritance levels.
Fragile Base Class Problem
[BEGINNER] Formally introduced in Chapter 5: a seemingly safe change to a base class breaks subclasses in ways neither the base class author nor the subclass authors could fully predict, because subclasses depend on the base class's implementation, not just its public contract.
The recurring shape: a base class method is changed (a new side effect added, an internal call order changed), and a subclass that overrode a different method — but relied on the old call sequence or side effects — breaks silently, often without a compiler error.
Fix: favor composition (Chapter 7) to avoid the coupling entirely; where inheritance is kept, document (or test) exactly what's part of the stable contract versus what's an internal implementation detail subclasses must not rely on.
Anemic Domain Model
[BEGINNER] Classes that are pure data holders — all fields, getters, and setters, with zero business logic — while all the actual behavior lives in separate "service" classes that operate on them from outside. This is the encapsulation failure from Chapter 3 taken to its logical extreme across an entire codebase.
// Anemic: Order is just a data bag.
class Order {
id: string = "";
items: { priceCents: number; quantity: number }[] = [];
status: string = "pending";
}
// All behavior lives outside, operating on Order's public fields directly.
class OrderService {
calculateTotal(order: Order): number {
return order.items.reduce((sum, i) => sum + i.priceCents * i.quantity, 0);
}
cancel(order: Order): void {
if (order.status === "shipped") throw new Error("Cannot cancel a shipped order");
order.status = "cancelled";
}
}
// Nothing stops this, anywhere in the codebase:
const order = new Order();
order.status = "cancelled"; // bypasses OrderService.cancel()'s validation entirely
The invariant ("can't cancel a shipped order") exists only inside OrderService, exactly like Chapter 1's original bank account failure — any code with a reference to the Order can bypass it by setting status directly.
Fix: move behavior back onto the object that owns the data:
class Order {
private status: string = "pending";
constructor(private items: { priceCents: number; quantity: number }[]) {}
calculateTotal(): number {
return this.items.reduce((sum, i) => sum + i.priceCents * i.quantity, 0);
}
cancel(): void {
if (this.status === "shipped") throw new Error("Cannot cancel a shipped order");
this.status = "cancelled";
}
}
[SENIOR INSIGHT] Anemic domain models are common specifically in codebases that over-apply a layered architecture (Chapter 4) without noticing that "keep business logic in the service layer" was meant to apply to cross-entity orchestration, not to logic that rightfully belongs to a single entity's own invariants. Not every layering discipline should come at the cost of encapsulation.
Common mistakes
- Not recognizing an anti-pattern because it grew gradually. All five of these tend to arrive one small, reasonable-looking change at a time — recognizing them requires periodically zooming out, not just reviewing each diff in isolation.
- Fixing one anti-pattern by introducing another. Splitting a god object into ten classes with no clear ownership boundaries can just relocate the coupling problem rather than solve it (Chapter 9's over-splitting warning).
- Treating "anemic domain model" as always wrong. In codebases genuinely built around a functional-core/imperative-shell style, or around data pipelines with no meaningful per-entity invariants, plain data structures plus separate functions are a legitimate, deliberate choice (Chapter 1's "when OOP is the wrong tool") — the anti-pattern is specifically when invariants exist but aren't protected, not "having a class with public fields" in general.
- Diagnosing a symptom without naming the root cause. "This class is too big" is less useful in a review than "this looks like a god object — should we split by responsibility along the lines of user/payment/notification?"
Checklist: catching these anti-patterns early
- God object: does this class serve more than one clearly distinct stakeholder or responsibility? If new, unrelated methods keep landing here because "it's already wired up," that's the tell.
- Tight coupling: does this class construct its own concrete dependencies internally (
new SomeConcreteClass()) rather than receiving an abstraction? That's the DIP violation to look for. - Deep hierarchies: is there a third level of inheritance? If so, ask whether composition would model it more precisely before adding a fourth.
- Fragile base class: does a subclass rely on a base class's internal call order or side effects, rather than only its documented public contract?
- Anemic domain model: does an invariant about an entity's data live in a separate service class, while the entity itself exposes public setters that can bypass it entirely?
Discussion / interview questions
-
"What is a god object, and why does it tend to happen even on teams that know better?" Strong answer: A class accumulating unrelated responsibilities because it's the path of least resistance for each individual addition; happens gradually, which is why it requires periodic zoomed-out review rather than being caught by individual PR review alone.
-
"How would you detect tight coupling in a code review, concretely?" Strong answer: Looks for classes instantiating concrete dependencies directly inside their own methods/constructors (
new ConcreteThing()) instead of receiving an interface — ties this back to DIP (Chapter 10). -
"What is an anemic domain model, and is it always a mistake?" Strong answer: Data-only classes with all logic external; a mistake specifically when it lets external code bypass invariants that should be protected, but a legitimate, deliberate choice in codebases without meaningful per-entity invariants (e.g., functional-style data pipelines).
-
"Explain the fragile base class problem with an example different from the one in this chapter." Strong answer: Constructs a novel scenario — e.g., a base class method's internal helper call is reordered, and a subclass's override that assumed the old order now runs with stale state — showing understanding of the mechanism, not just reciting the name.
-
"You're asked to fix a 2,000-line 'God' class. What's your approach?" Strong answer: Identify the distinct responsibilities/stakeholders first (don't split arbitrarily), extract one cohesive responsibility at a time into its own class with tests, and avoid introducing a new anti-pattern (e.g., ten classes with tangled cross-dependencies) in the process.
Chapter summary / cheat sheet
| Anti-pattern | Root cause | Fix |
|---|---|---|
| God object | SRP violation, accreted gradually | Split by responsibility |
| Tight coupling | Depending on concrete classes instead of abstractions | Apply DIP; inject interfaces |
| Deep inheritance hierarchy | Reuse-driven inheritance without substitutability | Flatten; prefer composition |
| Fragile base class | Subclasses depend on base class implementation, not contract | Favor composition; document the real contract |
| Anemic domain model | Behavior separated from the data it protects | Move invariant-protecting logic back onto the entity |
- All five anti-patterns are traceable directly back to a principle from Parts 1–2 of this book being ignored, usually gradually rather than in one obvious step.
- Recognizing and naming them precisely is what makes a code review actionable ("this is a god object, split along X/Y/Z") instead of vague ("this feels messy").