SDE Path

The Four Pillars, As Interviewers Test Them

12 min read

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

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

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

  3. 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?'"

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

Check yourself

5 questions — graded when you submit.

Question 1

You're implementing a notification system. Users can receive Email, SMS, or Push notifications. A colleague suggests a base Notification class with a type field, then in the sender, a big if-else on type. Why is this problematic?

Question 2

In the refactored Payment class, why are amountInCents and status marked private instead of public?

Question 3

When should you use inheritance vs composition in an OOP design?

Question 4

Your PricingStrategy interface has methods computeFee() and getDescription(). Why is an interface better here than a base class with default implementations?

Question 5

You're refactoring a Vehicle hierarchy: Vehicle → Car, Vehicle → Boat. Cars honk, boats don't. What's the polymorphic fix?

Finished reading?

Sign in to save your progress across lessons.