Definition
The fragile base class problem occurs when a child class inherits methods from a parent that violate the child's intended behavior.
- Classic example: Java's
Stack extends Vector. Stack should be LIFO (push/pop/peek only). But inheriting from Vector exposesadd(0, item),remove(0),get(5),set(index, item): all break the LIFO contract. - Root cause: inheritance exposes the parent's entire public API. The child cannot hide unwanted methods.
- Fix: composition. Hold a private
Listinternally, expose onlypush,pop,peek. The internal list is hidden: callers cannot calladd(0)orremove(0). - Rule: never inherit just for code reuse. If your class IS-A list (should have all list methods), extend. If your class USES a list internally but is NOT a list, compose.
Imagine National Bank of India's priority queue for customer service: highest-priority customer served first. The developer inherits from ArrayList to reuse its storage. But now anyone can call queue.add(0, lowPriorityCustomer), inserting a low-priority customer at the front of the queue, bypassing the priority rules. The ArrayList methods leak through inheritance, breaking the queue's contract. Fix: hide the ArrayList inside a private field and expose only enqueue and dequeueHighestPriority.
Understanding the Concepts
What is the fragile base class problem?
The fragile base class problem occurs when inheritance exposes parent methods that break the child's contract.
How does Java's Stack demonstrate the problem?
// java.util.Stack extends Vector: the broken design in the JDK itself
java.util.Stack<String> stack = new java.util.Stack<>();
stack.push("A");
stack.push("B");
stack.push("C");
// Breaks LIFO contract:
stack.add(0, "Z"); // insert at bottom: Vector.add(index, element) leaked
stack.remove(0); // remove from bottom: Vector.remove(int) leaked
stack.get(1); // random access: Vector.get(int) leaked
stack.set(0, "REPLACED"); // replace arbitrary position: leaked
// The Stack's LIFO contract is destroyed.
// The Javadoc says: use Deque instead.
How does composition fix this?
package com.bank.util;
import java.util.ArrayList;
import java.util.EmptyStackException;
import java.util.List;
public class SafeStack<E> {
private final List<E> elements = new ArrayList<>(); // private: hidden
public void push(E item) {
elements.add(item);
}
public E pop() {
if (elements.isEmpty()) throw new EmptyStackException();
return elements.remove(elements.size() - 1);
}
public E peek() {
if (elements.isEmpty()) throw new EmptyStackException();
return elements.get(elements.size() - 1);
}
public boolean isEmpty() { return elements.isEmpty(); }
public int size() { return elements.size(); }
// No add(index, item), no remove(0), no get(index), no set(index, item).
// LIFO contract enforced. ArrayList is hidden.
}
What does a banking example look like?
// WRONG: inheritance leaks ArrayList methods
public class BrokenPriorityQueue extends ArrayList<Customer> {
public void enqueue(Customer customer) {
int index = findInsertionPoint(customer.getPriority());
add(index, customer);
}
public Customer dequeueHighest() {
if (isEmpty()) throw new java.util.NoSuchElementException();
return remove(0);
}
}
// Caller can bypass priority:
// queue.add(0, new Customer("Hacker", Priority.LOWEST)); // front of queue
// CORRECT: composition hides ArrayList
public class SafePriorityQueue {
private final java.util.List<Customer> queue = new ArrayList<>();
public void enqueue(Customer customer) {
int index = findInsertionPoint(customer.getPriority());
queue.add(index, customer);
}
public Customer dequeueHighest() {
if (queue.isEmpty()) throw new java.util.NoSuchElementException();
return queue.remove(0);
}
// No add(0, customer) exposed. Priority contract enforced.
}
What is the rule for when to inherit vs compose?
If your class IS-A list (it should have all list methods), then extend. If your class USES a list internally but is NOT a list (stack, queue, priority queue), then compose.
What are common mistakes?
The most common mistake is inheriting from a collection class for storage reuse.
// WRONG: inheriting for code reuse
public class AccountCache extends HashMap<String, Account> { }
// Exposes all HashMap methods: callers can call remove() to bypass cache invalidation.
// CORRECT: composition
public class AccountCache {
private final java.util.Map<String, Account> cache = new java.util.HashMap<>();
public Account get(String id) { return cache.get(id); }
public void put(String id, Account account) {
cache.put(id, account);
notifyCacheUpdate(id); // controlled side effect
}
}
What should a senior engineer know?
- The JDK added Deque (interface with push/pop) instead of fixing Stack, because Stack extends Vector and removing that inheritance would break backward compatibility.
- Effective Java Item 18: favor composition over inheritance, especially when you do not control the parent class.
- In Spring, services never extend collection classes. They compose repositories and other services as fields.
Visual Overview
FRAGILE BASE CLASS PROBLEM ────────────────────────────────────────── Stack extends Vector ┌─────────────────────────┐ │ Stack (LIFO contract) │ │ push(), pop(), peek() │ │ │ │ LEAKED from Vector: │ │ add(0, item) <- breaks LIFO │ remove(0) <- breaks LIFO │ get(index) <- breaks LIFO │ set(index, x) <- breaks LIFO └─────────────────────────┘ COMPOSITION FIX ┌─────────────────────────┐ │ SafeStack │ │ push(), pop(), peek() │ │ ┌───────────────────┐ │ │ │ private List │ │ HIDDEN │ │ elements │ │ │ └───────────────────┘ │ │ No add, remove, get, set│ │ LIFO enforced. │ └─────────────────────────┘
Key Takeaway
The fragile base class problem occurs when inheritance exposes parent methods that break the child's intended behavior. Java's own Stack extends Vector is the classic example: Stack should only support push/pop/peek (LIFO), but inheriting from Vector exposes add(0, item), remove(0), get(index), and set(index, item), all of which break the LIFO contract. The JDK Javadoc itself recommends using Deque instead.
The fix is composition: SafeStack holds a private ArrayList field and exposes only push, pop, peek. The dangerous add(0, x) method does not exist on SafeStack because the ArrayList is hidden. The LIFO contract is enforced by design.
Remember one sentence: never inherit just for code reuse; if your class uses a collection internally but is not a collection, compose with a private field and expose only the methods that make sense for your abstraction.
---