The four OOP pillars restated with real code (not textbook definitions), composition vs inheritance, interface vs abstract class, and how to read a design prompt in the first five minutes.
Published September 22, 2026
"Explain the four pillars of OOP" is often the first LLD question asked — and the one most candidates answer worst, because they recite definitions instead of showing they've internalized the tradeoffs. This lesson forces the second version.
Encapsulation — bundling data with the methods that operate on it, and controlling access.
class Account {
private double balance; // hidden
public void deposit(double amt) { if (amt > 0) balance += amt; } // controlled access
public double getBalance() { return balance; }
}
The point isn't "make fields private" — it's that Account can now enforce amt > 0 on every deposit, a rule that would be impossible to guarantee if balance were public and callers mutated it directly.
Abstraction — exposing what something does, hiding how.
interface PaymentGateway { void charge(double amount); }
A caller depends on PaymentGateway, never on whether it's Stripe or a bank API underneath — the implementation can change without the caller changing.
Inheritance — a subtype reuses and extends a supertype's behavior. class SavingsAccount extends Account inherits deposit/getBalance for free, adding interest-specific behavior.
Polymorphism — one interface, many implementations, resolved at runtime.
List<Account> accounts = List.of(new SavingsAccount(), new CheckingAccount());
for (Account a : accounts) a.applyMonthlyFee(); // each type applies its own rule
"Favor composition over inheritance" is a real engineering rule, not a slogan — it has a concrete failure mode behind it.
Where inheritance breaks down: class Penguin extends Bird looks reasonable until Bird has a fly() method — penguins can't fly, and now you're overriding fly() to throw an exception or silently do nothing, both of which violate the Liskov Substitution Principle (a Penguin is no longer safely substitutable wherever a Bird is expected — see the Liskov Substitution & Interface Segregation lesson).
The composition fix:
class Bird {
private final FlyBehavior flyBehavior; // composed, not inherited
Bird(FlyBehavior fb) { this.flyBehavior = fb; }
void fly() { flyBehavior.fly(); }
}
class CantFly implements FlyBehavior { public void fly() { /* no-op */ } }
Now a penguin is new Bird(new CantFly()) — no broken inheritance chain, no exception-throwing override. This is also literally the Strategy pattern (see the Single Responsibility & Open/Closed lesson for OCP, which this pattern also satisfies).
Comparable, Runnable). A class can implement many interfaces.A quick tell: if you're modeling "can do X" across unrelated types → interface. If you're modeling "is a kind of X" with shared internal logic → abstract class. Since Java 8's default methods, the line has blurred somewhat, but the single inheritance constraint on classes still makes this decision matter — reach for interfaces first, since they don't burn your one shot at extending a class.
Given a prompt like "Design a parking lot system", the fastest way in is:
This isn't a trick — it's literally how you convert an ambiguous English prompt into a first-pass class diagram in under five minutes, which is the single biggest time-pressure win in an LLD round. Interviewers watching a candidate freeze on "where do I even start" are watching this exact skill missing.
Q: Can you have encapsulation without private fields? A: Weakly — encapsulation is about controlling how state changes, not merely hiding it syntactically. A public field with no validation offers no encapsulation even if wrapped by a getter that does nothing; a well-designed public API on top of a private field is what actually delivers it.
Q: Is a record in modern Java still "encapsulated"?
A: Yes — its fields are private final under the hood; a record gives you encapsulation and immutability by default with far less boilerplate.
Q: If interfaces can have default methods now, why ever use an abstract class? A: Abstract classes can hold instance state (fields) — interfaces cannot (aside from constants). If subclasses need to share mutable or initialized state, not just behavior, an abstract class is still the right tool.
Q: Give an example where inheritance is the right choice, not composition.
A: A strict, stable "is-a" hierarchy with genuinely shared invariants — e.g. Circle/Square extending a Shape that only exposes area()/perimeter() with no behavior that could break substitutability, unlike the Bird/Penguin case above.