Command Palette

Search for a command to run...

PHASE 16Advanced ~34 min· topic 2 of 4

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.

Main.javawhole filejava
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.

SOLID through one refactordiagram
Rendering diagram…

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).

Composition beats inheritance: why, concretelydiagram
Rendering diagram…

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.

Main.javawhole filejava
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.

The LLD answer structurediagram
Rendering diagram…

Try it yourself

  1. 1

    Remove the double count

    In the first example, delete the addAll override from CountingHashSet. Predict the count. It is now 3, but only because of how AbstractCollection.addAll happens to be written. That fragility is the real lesson.

  2. 2

    Add a rule without touching Checkout

    In the Strategy example, add class FlatFiveOff implements DiscountRule that subtracts 500 cents, and add it to the list. Confirm Checkout needed no edit: that's the open/closed principle at work.

  3. 3

    Fix Liskov with immutability

    Rewrite the shapes as record Rect(int w, int h) with an area() method and a static factory Rect square(int side). There are no setters, so no contract can be broken.

Code & diagrams

Inheritance counts twice, composition counts once Java 9+ New tab

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.

Sign in to run this example in your browser.

Expected output

inheritance: size 3, counted 6
composition: size 3, counted 3
Shallow immutability and the defensive copy Java 16+ New tab

A 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.

Sign in to run this example in your browser.

Expected output

LeakyTeam[name=leaky, members=[ana, bo, mallory]]
SafeTeam[name=safe, members=[ana, bo]]
safe.members().add: UnsupportedOperationException
Open/closed and dependency inversion with Strategy Java 9+ New tab
Sign in to run this example in your browser.

Expected output

NoDiscount       110.00
StudentDiscount  99.00
BulkDiscount     95.00
The Liskov violation, made visible New tab
Sign in to run this example in your browser.

Expected output

Rectangle: expected 20, got 20
Square: expected 20, got 16
Three correct singletonsjava

In 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.

terminal
$ java Main.java
── what you'll see ──
p.equals(cp): true
cp.equals(p): false

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".

terminal
$ java Main.java
── what you'll see ──
status: null

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

01

Why not put retry logic in each channel?

02

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 final and 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 String does with StringBuilder.

  • ▸

    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. 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. 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. 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 HashSet to count additions counts twice, because addAll secretly calls add.

  4. 4

    Immutability (Topic 4.11) is the second. An immutable object never changes after construction: final class, private final fields, 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 a List component is only *shallowly* immutable unless you copy the list.

  5. 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. 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

01

[Pillars] Explain the four pillars of OOP with one example each.

02

[Pillars] What is the difference between abstraction and encapsulation?

03

[SOLID] Explain the SOLID principles.

04

[SOLID] Give an example of a Liskov substitution violation.

05

[SOLID] What is dependency inversion, and how is it different from dependency injection?

06

[Composition] Why prefer composition over inheritance? When is inheritance right?

07

[Relationships] What is the difference between association, aggregation and composition?

08

[Immutability] How do you make a class immutable, and why bother?

09

[Immutability] Why can't you subclass and add fields while keeping equals correct?

10

[Patterns] How do you write a thread-safe Singleton, and what are its problems?

11

[Patterns] Factory vs Builder: when do you use each?

12

[Patterns] Strategy vs State?

13

[Patterns] Name design patterns used in the JDK.

14

[Design] What are coupling and cohesion, and what is the Law of Demeter?

15

[Design] How do you approach a "design a parking lot" question?

Practice

01

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.

02

Write an immutable Period class holding a start and end int day, rejecting end before start, with a withEnd method.

03

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.