In a timed machine-coding interview, the four OOP pillars aren't abstract ideals—they're concrete refactoring levers that turn brittle code into maintainable systems. Interviewers watch how you apply them under pressure.
Encapsulation: Hide Internal State
What it is: Data and behavior are bundled together; internal state is private, changes visible only through a controlled interface.
Why interviewers test it: They see if you naturally hide implementation details and change your mind cleanly. If your class members are public and scattered across callers, you've failed to encapsulate.
Example: A bad Payment class
class Payment {
public int amountInCents;
public String status;
public long timestamp;
}
// Callers do this:
Payment p = new Payment();
p.amountInCents = 1000;
p.status = "pending"; // magic string, no validation
Refactored: Encapsulation with validation
class Payment {
private int amountInCents;
private PaymentStatus status;
private long timestamp;
public Payment(int amountInCents) {
if (amountInCents <= 0) throw new IllegalArgumentException();
this.amountInCents = amountInCents;
this.status = PaymentStatus.PENDING;
this.timestamp = System.currentTimeMillis();
}
public void markCompleted() {
if (status != PaymentStatus.PENDING) throw new IllegalStateException();
this.status = PaymentStatus.COMPLETED;
}
public int getAmountInCents() { return amountInCents; }
public PaymentStatus getStatus() { return status; }
}
In the interview: Say "I'll keep amount and status private so callers can't set invalid combinations. This lets me add constraints—like checking pending before completing—without breaking every caller if I refactor."
Abstraction: Expose Only What Matters
What it is: Hide complexity behind a simple interface. Callers see the what, not the how.
Why interviewers test it: Abstraction shrinks cognitive load. It makes interfaces stable when internals change. A good abstraction means you can swap implementations on day 2 of the interview.
Example: A leaky PaymentProcessor
class PaymentProcessor {
public boolean process(Payment p, String apiKey, int retries, long timeoutMs) {
// 50 lines of retry logic, HTTP calls, parsing...
}
}
Callers have to know: retry strategy, timeout, API key format. That's not their problem.
Refactored: Clean abstraction
interface PaymentGateway {
boolean charge(Payment p);
}
class StripeGateway implements PaymentGateway {
private final String apiKey;
private static final int MAX_RETRIES = 3;
private static final long TIMEOUT_MS = 5000;
public StripeGateway(String apiKey) {
this.apiKey = apiKey;
}
@Override
public boolean charge(Payment p) {
// retry logic, timeouts—all hidden
}
}
// Caller:
PaymentGateway gateway = new StripeGateway(apiKey);
gateway.charge(payment); // simple, stable
In the interview: "I'm defining a PaymentGateway interface so the caller doesn't know or care how retries work. If we add rate limiting tomorrow, only Stripe's code changes."
Inheritance: Reuse Behavior via Hierarchy
When it's used correctly: Base classes capture shared behavior, not just shared fields. A Notification base class makes sense if Email, SMS, and Push all validate, retry, and log the same way.
When it becomes a trap: Deep hierarchies, rigid is-a relationships, tight coupling. Most teams learn: composition beats inheritance. But inheritance still appears in interviews—especially adapter patterns or template methods.
Example: A fragile hierarchy
class Vehicle {
public void drive() { }
public void refuel() { }
public void honk() { }
}
class Car extends Vehicle { }
class Boat extends Vehicle {
// Inherits refuel() but boats don't refuel that way.
// Inherits honk() which doesn't make sense.
}
Refactored: Composition wins
interface Vehicle {
void move();
}
interface Fueled {
void refuel();
}
interface Audible {
void honk();
}
class Car implements Vehicle, Fueled, Audible {
@Override
public void move() { }
@Override
public void refuel() { }
@Override
public void honk() { }
}
class Boat implements Vehicle, Fueled {
// Boat is a vehicle, can refuel, but has no honk()
@Override
public void move() { }
@Override
public void refuel() { }
}
In the interview: "I'll use composition via interfaces rather than a deep inheritance tree. Each class implements only the behaviors it actually has. That way, if I later add a SailingBoat that doesn't have an engine, I don't have a nonsensical hierarchy."
Polymorphism: One Interface, Many Implementations
What it is: Call a method on an interface ref, different classes run different code. No switch statements on type.
Why interviewers test it: Polymorphism kills the switch/if-ladder trap. It's the difference between fragile and resilient code.
Example: Type-based dispatch (the smell)
class NotificationSender {
public void send(Notification n) {
if (n.type == "EMAIL") {
// send email...
} else if (n.type == "SMS") {
// send sms...
} else if (n.type == "PUSH") {
// send push...
}
}
}
// Adding a new type means finding and editing this class. Fragile.
Refactored: Polymorphism
interface Notification {
void send();
}
class EmailNotification implements Notification {
@Override
public void send() { /* send email */ }
}
class SMSNotification implements Notification {
@Override
public void send() { /* send sms */ }
}
class NotificationSender {
public void send(Notification n) {
n.send(); // polymorphic call, no type checks
}
}
// Adding a new type (e.g., Slack) just means a new class that implements Notification.
// NotificationSender stays the same.
In the interview: "I'll define a Notification interface with a send() method. Each concrete notification type implements it. Now the sender doesn't care what type it is—it just calls send(). Adding a new channel next sprint requires no changes to the sender."
Bringing It Together: A Parking Lot Example
Imagine you're designing a parking lot system. Here's how the pillars work:
- Encapsulation: ParkingSpot hides whether it's occupied; only SpotReservation can change that via reserve() and release().
- Abstraction: PricingStrategy interface hides hourly-rate vs flat-fee logic; only the strategy knows. Clients just call computeFee(hours).
- Inheritance/Composition: Don't inherit Vehicle → Car → ElectricCar. Instead, use composition: a Vehicle has a FuelType (GAS, ELECTRIC, HYBRID).
- Polymorphism: PricingStrategy implementations (HourlyPricing, FlatRate, MemberDiscount) are called polymorphically via the interface. Adding a new pricing model means writing one new class, not editing the lot.
In the interview
-
Ask clarifying questions about scope before coding. If they ask for a parking lot, don't assume you know what "pricing" means. Encapsulation depends on knowing what must stay private.
-
Use interfaces, not base classes. When two behaviors share code, ask yourself first: do they share state, or just contracts? If it's just contracts, compose with interfaces.
-
Avoid switch statements on type. The moment you see a type tag, reach for an interface and polymorphism. Tell the interviewer: "I'll define a Strategy interface so we never have to ask 'what type is this?'"
-
Validate at the boundary. Encapsulation isn't just hiding data—it's enforcing invariants. If a Payment can't be negative, check it in the constructor, not in callers.