Topic 16.2
OOP and Design Interview Questions
In one line
The object-oriented design questions interviewers use to test judgement: the four pillars, SOLID, composition over inheritance, immutability, the common patterns and the "design a class for…" prompt, with runnable proofs and links to the System Design course's pattern pages.
Think of it like this
A LEGO set and a sculpture can look the same on the shelf. But you can take a LEGO car apart, swap its wheels for bigger ones and add a trailer without breaking anything, because each brick has a standard connector. Good object-oriented design is LEGO: small parts with clear connectors (interfaces) that you can swap and extend. Design interviews check whether you build LEGO or sculptures.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Coupling
- How much one class depends on the details of another. Low coupling means you can change one without touching the other.
- Cohesion
- How closely the parts of one class belong together. High cohesion means the class does one job well.
- Abstraction
- A simplified view that shows what something does and hides how it does it, such as an interface.
- Composition
- Building an object out of other objects it holds as fields, instead of inheriting from a parent class.
- Defensive copy
- Copying a mutable object as it comes in or goes out, so outside code can't change your object's insides.
- Design pattern
- A named, reusable shape of classes that solves a common design problem, such as Strategy or Builder.
- Invariant
- A rule about an object's state that must always be true, such as "balance is never negative".
- LLD
- Low-level design: designing the classes, interfaces and their relationships for one feature or system.
Step by step
01Answer pillar questions with a story
"What is encapsulation?" Weak answer: "private fields with getters and setters". Strong answer: "an object protects its invariants. A BankAccount with a public balance can be set to -500 by anyone; with a private field and a withdraw method that rejects overdrafts, the rule lives in one place and can't be bypassed." Note that a getter and setter for every field is *not* encapsulation: it exposes the data with extra steps.
class BankAccount {
private long balanceCents; // hidden state
void withdraw(long cents) {
if (cents <= 0) throw new IllegalArgumentException("amount must be positive");
if (cents > balanceCents) throw new IllegalStateException("insufficient funds");
balanceCents -= cents; // invariant: never negative
}
}02SOLID through one refactor
Start with a Checkout that computes a discount with an if chain on the customer type. Every new customer type means editing Checkout: it violates open/closed. Extract an interface DiscountRule, give each customer type its own implementation, and inject the rule through the constructor (dependency inversion). Now a new rule is a new class, Checkout is untouched, and tests can pass a fake rule.
This is exactly the Strategy pattern (System Design course, /topic/phase-2/strategy). The third runnable example below runs it.
03Liskov: the Square and Rectangle trap
Mathematically a square *is a* rectangle, so class Square extends Rectangle looks right. But a mutable Rectangle promises that setWidth leaves the height alone. Square must break that promise to stay square, so code written for Rectangle (set width 5, height 4, expect area 20) gets 16. The subclass is not substitutable: it violates LSP.
The lesson interviewers want: "is-a" in the real world is not enough; a subclass must honour the parent's behavioural contract. Fixes: make shapes immutable (a square then simply is a rectangle with equal sides), or don't relate them by inheritance at all and use a sealed Shape interface (Topic 5.11).
04Composition beats inheritance: why, concretely
Inheritance exposes you to the parent's self-use: which of its own methods call which others. HashSet.addAll (inherited from AbstractCollection) calls add for each element. A subclass that counts in both add and addAll therefore counts every element twice. Nothing in HashSet's public documentation for add warns you, and a future JDK could change it.
A wrapper (forwarding class) that *holds* a Set and delegates to it sees only the calls you make; it can't be broken by the inner set's private habits. This is also the Decorator pattern (/topic/phase-2/decorator). Use inheritance only for a true is-a relationship where the parent was designed for extension (Topic 5.12).
05Building an immutable class, rule by rule
1) Declare the class final (or make constructors private and use factories) so no subclass adds mutable state. 2) Make every field private final. 3) No setters; "changing" methods return a new object (withPrice(…)), as String and java.time do. 4) Copy mutable inputs in the constructor and never return internal mutable objects; return copies or unmodifiable views. 5) Don't let this escape during construction.
With a record, rules 1 to 3 are automatic; you add rule 4 in a compact constructor: items = List.copyOf(items);. The second runnable example shows what happens without it.
record Order(String id, List<String> items) {
Order { // compact constructor
Objects.requireNonNull(id);
items = List.copyOf(items); // defensive copy, also unmodifiable
}
}06Patterns: name the problem first
Never say "I'd use a Factory here" without the problem it solves. "Object creation depends on a config value and callers shouldn't know the concrete classes": Factory (/topic/phase-2/factory). "A constructor with seven optional parameters": Builder (/topic/phase-2/builder). "Exactly one shared instance": Singleton (/topic/phase-2/singleton), ideally an enum or injected by a framework. "Notify many listeners of a change": Observer (/topic/phase-2/observer). "Add behaviour to an object without subclassing": Decorator.
Over-patterning is a red flag. A StrategyFactoryProvider for two if branches is worse design than the if.
07The LLD answer structure
Spend the first minutes on requirements: who uses it, the main use cases, what is out of scope. Then entities: for a library, Book, Copy, Member, Loan, Library. Then responsibilities: who checks borrow limits? (Library or a BorrowPolicy, not Book). Then relationships (a Book has many Copy objects; a Loan links a Member and a Copy). Then code the core and walk one flow: borrow, return, fine.
Call out extension points you deliberately left (a new member type is a new BorrowPolicy class), and concurrency if two members can borrow the last copy at the same moment. Topic 16.4 builds this exact system.
Try it yourself
- 1
Remove the double count
In the first example, delete the
addAlloverride fromCountingHashSet. Predict the count. It is now 3, but only because of howAbstractCollection.addAllhappens to be written. That fragility is the real lesson. - 2
Add a rule without touching Checkout
In the Strategy example, add
class FlatFiveOff implements DiscountRulethat subtracts 500 cents, and add it to the list. ConfirmCheckoutneeded no edit: that's the open/closed principle at work. - 3
Fix Liskov with immutability
Rewrite the shapes as
record Rect(int w, int h)with anarea()method and a static factoryRect square(int side). There are no setters, so no contract can be broken.
Code & diagrams
This is the example from Effective Java (Item 18). The subclass depends on an implementation detail of HashSet that its own documentation doesn't promise.
Expected output
inheritance: size 3, counted 6
composition: size 3, counted 3A record's fields are final, but a final reference to a mutable list still lets anyone holding the same list change it. List.copyOf takes a snapshot and returns an unmodifiable list.
Expected output
LeakyTeam[name=leaky, members=[ana, bo, mallory]]
SafeTeam[name=safe, members=[ana, bo]]
safe.members().add: UnsupportedOperationExceptionExpected output
NoDiscount 110.00
StudentDiscount 99.00
BulkDiscount 95.00Expected output
Rectangle: expected 20, got 20
Square: expected 20, got 16In application code, prefer letting a dependency-injection container manage a single instance (/topic/phase-2/dependency-injection). Static singletons are global state and are hard to replace in tests.
// 1. Enum: thread-safe, serialization-safe, reflection-safe. Effective Java's choice.
enum IdGenerator {
INSTANCE;
private final java.util.concurrent.atomic.AtomicLong next = new java.util.concurrent.atomic.AtomicLong();
long nextId() { return next.incrementAndGet(); }
}
// 2. Holder idiom: lazy, thread-safe, no locking. The JVM initialises Holder
// only on the first call to get(), and class initialisation is thread-safe.
final class Config {
private Config() {}
private static final class Holder { static final Config INSTANCE = new Config(); }
static Config get() { return Holder.INSTANCE; }
}
// 3. Double-checked locking: lazy; the field MUST be volatile (Topic 13.4),
// or another thread may see a reference to a half-constructed object.
final class Registry {
private static volatile Registry instance;
private Registry() {}
static Registry get() {
Registry r = instance;
if (r == null) {
synchronized (Registry.class) {
r = instance;
if (r == null) instance = r = new Registry();
}
}
return r;
}
}Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Break symmetry with inheritance in equals
Give Point(x, y) an equals that checks instanceof Point, then add ColorPoint extends Point with a color field whose equals checks instanceof ColorPoint and compares color too. Compare both ways.
Break #2
Call an overridable method from a constructor
In a parent constructor, call describe(), and override describe() in a subclass to print a subclass field initialised to "ready".
Myth vs fact
Myth
Encapsulation means private fields with getters and setters.
Fact
Encapsulation means the object guards its own rules. A setter for every field exposes the state as much as a public field does.
Myth
Inheritance is the main way to reuse code in OOP.
Fact
Composition is usually the better tool. Inheritance couples you to a parent's implementation; reserve it for real is-a relationships with classes designed for extension.
Myth
Using more design patterns makes a design better.
Fact
A pattern earns its place by solving a stated problem. Patterns without a problem add indirection and make code harder to read.
Myth
A record is fully immutable.
Fact
A record's fields are final, but if a component is a mutable object such as an ArrayList, its contents can still change. Copy it in the compact constructor.
Interview problem
The problem
Design a notification sender
"Our app sends notifications by email today. Product wants SMS and push next quarter, users choose their channels, and failed sends must be retried. Design the classes in Java."
You're given
- Adding a channel must not change existing classes.
- A user can have several channels.
- Failures are retried up to 3 times; the design must be unit-testable.
The interviewer follows up
Why not put retry logic in each channel?
How would you add a channel at run time without a restart?
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Effective Java's rule "design and document for inheritance or else prohibit it" is why the JDK marks so many classes
finaland why records, enums and sealed hierarchies exist. Sealed interfaces (Topic 5.11) let you choose inheritance deliberately and keep the hierarchy closed. - ▸
Immutability has a cost: every change allocates. Modern GCs make short-lived objects very cheap (allocation is a pointer bump in a thread-local buffer, and young-generation collection cost is proportional to *live* objects), so this is rarely a problem; when it is, use a mutable builder internally, as
Stringdoes withStringBuilder. - ▸
Dependency inversion is what makes unit testing cheap: a class that receives its collaborators through its constructor can be tested with fakes and no framework. Spring's constructor injection is this principle automated (
/topic/phase-2/dependency-injection). - ▸
Interface segregation shows up in the JDK:
Iterable,Collection,List,SequencedCollection(Java 21) are layered so code can ask for the smallest capability it needs. Accept the narrowest type that works, return the most specific useful one.
Remember this
- 1
The four pillars are the vocabulary. Encapsulation: an object hides its data and guards its rules behind methods (Topic 4.5). Abstraction: callers see *what* an object does, not *how* (Topic 5.7). Inheritance: a class reuses and specialises another (Topic 5.1). Polymorphism: one call, many behaviours, chosen at run time by the object's class (Topic 5.4). Interviewers rarely want the definitions; they want an example where each one *saved* you from a problem.
- 2
SOLID is five principles for code that is easy to change. Single responsibility: a class has one reason to change. Open/closed: add behaviour by adding classes, not by editing working ones. Liskov substitution: a subclass must work anywhere its parent is expected. Interface segregation: many small interfaces beat one fat one. Dependency inversion: depend on abstractions, and receive dependencies from outside. The System Design course explains each in depth at
/topic/phase-1/solid. - 3
Composition over inheritance (Topic 5.12) is the most tested design idea. Inheritance couples a subclass to its parent's *implementation*, including calls you can't see; composition holds another object as a field and calls only its public API. The first runnable example below shows the famous case where extending
HashSetto count additions counts twice, becauseaddAllsecretly callsadd. - 4
Immutability (Topic 4.11) is the second. An immutable object never changes after construction:
finalclass,private finalfields, no setters, defensive copies of mutable inputs and outputs. It is thread-safe without locks, safe as a map key, and easy to reason about. Records (Topic 4.9) give you most of this for free, but a record with aListcomponent is only *shallowly* immutable unless you copy the list. - 5
Design patterns are named solutions to recurring problems. Expect Singleton, Factory, Builder, Strategy, Observer, Decorator, Adapter and Template Method, and be able to point to one in the JDK:
Collections.unmodifiableList(Decorator/Proxy),Comparator(Strategy),InputStream(Template Method and Decorator),Arrays.asList(Adapter),StringBuilder(Builder). Each pattern has a full page in the System Design course under/topic/phase-2/…. - 6
"Design a parking lot / library / vending machine" is an LLD (low-level design) interview. The structure that scores well: clarify requirements, list the entities (nouns) and their responsibilities (verbs), draw the relationships, pick patterns only where they solve a stated need, then write the core classes and walk through one use case. The System Design course's LLD phase (
/topic/phase-3/lld-walkthrough) and worked problems (/topic/phase-4/parking-lot,/topic/phase-4/library-management) show full answers; Topic 16.4 builds a library system in Java.
Explain it without notes
[Pillars] Explain the four pillars of OOP with one example each.
[Pillars] What is the difference between abstraction and encapsulation?
[SOLID] Explain the SOLID principles.
[SOLID] Give an example of a Liskov substitution violation.
[SOLID] What is dependency inversion, and how is it different from dependency injection?
[Composition] Why prefer composition over inheritance? When is inheritance right?
[Relationships] What is the difference between association, aggregation and composition?
[Immutability] How do you make a class immutable, and why bother?
[Immutability] Why can't you subclass and add fields while keeping equals correct?
[Patterns] How do you write a thread-safe Singleton, and what are its problems?
[Patterns] Factory vs Builder: when do you use each?
[Patterns] Strategy vs State?
[Patterns] Name design patterns used in the JDK.
[Design] What are coupling and cohesion, and what is the Law of Demeter?
[Design] How do you approach a "design a parking lot" question?
Practice
Refactor a class ReportService that loads data, formats it as CSV and writes it to a file into classes with one responsibility each. Show the interfaces and a main that wires them together.
Write an immutable Period class holding a start and end int day, rejecting end before start, with a withEnd method.
Write a LoggingList<E> that wraps any List<E> by composition and prints every add, exposing only add, get and size.
Trade-offs
- ↔
Abstraction vs simplicity: an interface with one implementation adds a file and an indirection. Add it when there's a real second implementation, a test seam, or a module boundary.
- ↔
Immutability vs allocation: immutable objects are safe and simple but create a new object per change. In very hot loops a confined mutable builder can be faster.
- ↔
Inheritance vs composition: inheritance gives polymorphism and reuse with less code; composition gives looser coupling and run-time flexibility at the cost of forwarding methods.
Done when you can
Done when you can explain each SOLID principle with a Java example and a violation.
Done when you can show why extending HashSet to count additions fails, and fix it with composition.
Done when you can write an immutable class and a record with a defensive copy.
Done when you can write a thread-safe Singleton three ways and explain why the enum is preferred.
Done when you can name the problem each common pattern solves and point to a JDK example.
Done when you can structure an LLD answer from requirements to a walked-through flow.