Vasu Patel logoVasu Patel
← Objects First: OOP Fundamentals
All chapters in this track
  1. 01Chapter 1: Why Object Orientation Exists
  2. 02Chapter 2: Classes, Objects, and Instances, Precisely Defined
  3. 03Chapter 3: The Four Pillars I: Encapsulation
  4. 04Chapter 4: The Four Pillars II: Abstraction
  5. 05Chapter 5: The Four Pillars III: Inheritance
  6. 06Chapter 6: The Four Pillars IV: Polymorphism
  7. 07Chapter 7: Composition vs. Inheritance
  8. 08Chapter 8: Interfaces and Abstract Classes: When to Use Which
  9. 09Chapter 9: SOLID, Part 1: Single Responsibility & Open/Closed
  10. 10Chapter 10: SOLID, Part 2: Liskov Substitution, Interface Segregation, Dependency Inversion
  11. 11Chapter 11: Basic UML for Communicating Design
  12. 12Chapter 12: Anti-Patterns: God Objects, Tight Coupling, Deep Hierarchies, the Fragile Base Class, Anemic Domain Models
  13. 13Chapter 13: OOP Idioms Across TypeScript and Python — and Where OOP Is the Wrong Tool
  14. 14Chapter 14: When OOP Breaks Down: Real-World Failure Modes
  15. 15Chapter 15: Design Exercise: Parking Lot System
  16. 16Chapter 16: Design Exercise: Library Management System
  17. 17Chapter 17: Design Exercise: Elevator Controller
  18. 18Chapter 18: OOP in Interviews: What a Strong Answer Looks Like

Chapter 14 of 18

Chapter 14: When OOP Breaks Down: Real-World Failure Modes

Why this matters

Every chapter so far has shown OOP working well when applied with judgment. This chapter is the counterweight: real, recurring situations where careful, "correct" OOP design still produces a worse outcome than a simpler alternative — not because the engineers did it wrong, but because the paradigm itself is a poor match for the problem's actual shape. Recognizing these situations is a senior-level skill precisely because it requires resisting the pull of the tool you know best.

Failure mode 1: Premature abstraction

[SENIOR INSIGHT] Introduced in Chapter 4 and referenced throughout — this is common enough to deserve its own worked example. A team builds an interface and multiple "pluggable" implementations for a concept that, months later, still has exactly one real implementation.

// Built "for flexibility" — an interface, a factory, and a single real implementation.
interface ReportFormatter {
  format(data: object): string;
}
class JsonReportFormatter implements ReportFormatter {
  format(data: object): string { return JSON.stringify(data); }
}
class ReportFormatterFactory {
  static create(type: string): ReportFormatter {
    if (type === "json") return new JsonReportFormatter();
    throw new Error("Unknown formatter type");
  }
}
// Eighteen months later: still only ever called with "json". The interface,
// the factory, and the indirection have never once paid for themselves —
// every reader has to trace through three files to find one string concatenation.

The cost isn't hypothetical: every engineer who reads this code has to navigate the interface, the factory, and the implementation to understand what is, in reality, a single function. The fix — collapsing it back to a plain function — is itself a real refactor that has to be justified and executed later, at a cost that could have been avoided by waiting for a second real need before generalizing.

The rule of thumb this book uses throughout: build the abstraction when the second real implementation shows up, not in anticipation of one. This isn't dogma against ever planning ahead — it's a recognition that guessed-at future requirements are wrong often enough that the cost of premature generalization usually exceeds the cost of a later, informed refactor.

Failure mode 2: Modeling mutable shared state under concurrency

[BEGINNER] Encapsulation (Chapter 3) protects an object's invariants against sequential misuse — but it does not, by itself, protect against concurrent misuse. Two threads calling withdraw() on the same Account object at the same time can both pass the balance check before either one applies its deduction, corrupting the balance despite the method being "correctly" encapsulated.

class Account {
  private balanceCents = 1000;
  withdraw(amountCents: number): void {
    if (amountCents > this.balanceCents) throw new Error("Insufficient funds");
    // If another thread's withdraw() runs between the check above and the line below,
    // both withdrawals can succeed even though the account can't cover both.
    this.balanceCents -= amountCents;
  }
}

This isn't a flaw in the encapsulation — it's a reminder that encapsulation solves the "who can change this" problem, not the "what happens when two changes overlap in time" problem. That second problem is what synchronization primitives, immutability, and message-passing architectures exist to solve (the full subject of the companion Concurrency & Multithreading book) — OOP's tools alone don't cover it, and pretending they do is a common, costly mistake.

Failure mode 3: Deep hierarchies under changing requirements

[IN PRACTICE] Revisiting Chapter 12's deep-hierarchy anti-pattern from a different angle: even a hierarchy that was substitutable and well-designed at the time it was built can become a liability as requirements evolve in ways the original design didn't anticipate. A PaymentMethod → CreditCard → CorporateCreditCard hierarchy, sound when built, can become a straitjacket the day a requirement arrives that cuts across the hierarchy sideways (e.g., "any payment method can now optionally support recurring billing," a capability unrelated to the existing is-a structure). This is not a sign the original design was wrong — it's evidence that no static hierarchy can anticipate every future axis of variation, which is exactly why composition (Chapter 7) is generally safer for long-lived systems: it doesn't need to guess the right hierarchy up front.

Failure mode 4: Distributed systems and serialization boundaries

[SENIOR INSIGHT] OOP's core idea — behavior travels with data — breaks down at a network or serialization boundary. When an object is serialized to JSON and sent across a service boundary, its methods do not travel with it; only its data does. Code that assumes "if I have this object, I can call its methods" quietly stops being true the moment that object has been through a network hop, a message queue, or a database round-trip and come back as a plain data structure with no behavior attached — which is exactly why so many distributed-systems and microservice codebases lean toward largely anemic-looking data models (Chapter 12) for anything that crosses a boundary, keeping behavior localized to the service that owns the data, rather than fighting to reconstruct rich objects on every hop.

Common mistakes

  1. Treating every one of these failure modes as "OOP is bad." Each is specific and situational — OOP still solves the problems described in Chapters 1–13 for the many cases where they apply; the point is recognizing where it doesn't, not abandoning it wholesale.
  2. Building speculative abstractions to "future-proof" against failure mode 3, which is itself failure mode 1 — guessing at future axes of variation is exactly the premature-abstraction trap, whether the guess is about interfaces or about hierarchy shape.
  3. Assuming encapsulation alone makes an object thread-safe. It doesn't — see failure mode 2. Thread safety requires deliberate synchronization design on top of encapsulation, not instead of it.
  4. Fighting to keep behavior attached to data across a network boundary (trying to "rehydrate" full objects with methods on every service call) instead of accepting that data and behavior naturally separate at that boundary, and designing accordingly.

Checklist: recognizing when OOP is straining against the problem

  1. Is the abstraction being built for a guessed-at future need, or a current, real one? If guessed-at, wait (failure mode 1).
  2. Will this object's state be accessed by more than one thread/process concurrently? If so, encapsulation alone isn't sufficient — synchronization or immutability needs explicit design (failure mode 2).
  3. Does the current hierarchy need a cross-cutting capability that doesn't fit its is-a axis? That's a sign to introduce composition alongside (or instead of) extending the hierarchy further (failure mode 3).
  4. Will this object cross a network, queue, or storage boundary? If so, design for data-only transfer at that boundary rather than assuming behavior travels with it (failure mode 4).

Discussion / interview questions

  1. "Describe a situation where 'correct' object-oriented design still led to a worse outcome than a simpler approach." Strong answer: Picks one of the four failure modes and gives a concrete, plausible scenario — e.g., an interface with one real implementation that never paid for its indirection.

  2. "Does encapsulation make an object safe to share across threads?" Strong answer: No — explains the check-then-act race condition example, and correctly distinguishes "who's allowed to change this" (encapsulation) from "what happens when two changes overlap in time" (a concurrency concern requiring separate tools).

  3. "Why do many distributed systems end up with data models that look anemic (Chapter 12), even on teams that value rich domain models elsewhere?" Strong answer: Behavior doesn't survive serialization across a network boundary — only data does — so objects that cross that boundary are naturally treated as data-only, with behavior kept local to the service that owns the data.

  4. "How do you avoid over-correcting into premature abstraction while still designing for known future needs?" Strong answer: Distinguishes a known, concrete near-term requirement (worth designing for now) from a speculative future one (wait for the second real case) — doesn't treat "avoid premature abstraction" as "never think ahead."

  5. "A payment method hierarchy needs a new capability that cuts across every existing subclass. What would you consider changing?" Strong answer: Recognizes this as a cross-cutting concern poorly suited to the existing is-a hierarchy, and proposes extracting it as a composed capability (an interface + injected implementation, per Chapter 7) rather than forcing it into every hierarchy branch.

Chapter summary / cheat sheet

  • Premature abstraction: building interfaces/hierarchies for a guessed-at future need costs real navigation overhead now, for a benefit that may never materialize. Wait for the second real case.
  • Concurrency: encapsulation protects against sequential misuse, not concurrent misuse — check-then-act races can violate an invariant even in well-encapsulated code.
  • Hierarchy rigidity: even a well-designed hierarchy can't anticipate every future cross-cutting requirement — composition tends to age better for long-lived systems.
  • Serialization boundaries: behavior doesn't survive a network hop — only data does — which is why distributed systems often (correctly) look more anemic at their boundaries than a single-process domain model would.
  • None of these are arguments against OOP generally — they're specific, recognizable situations where its core assumptions stop holding, and where a senior engineer reaches for a different tool instead of forcing the familiar one.