Skip to content

Polymorphism and Interfaces

TL;DR: Polymorphism lets you write code that works with any type that satisfies a contract, without knowing the concrete class. Interfaces define that contract. Together, they eliminate type-checking if/else chains and make your code extensible without modification. This is the backbone of every major design pattern.

Polymorphism: One Interface, Many Implementations

Why This Matters in Interviews

Every design pattern you'll use in LLD interviews -- Strategy, Observer, Factory -- relies on polymorphism. If you don't reach for interfaces naturally, you'll end up with rigid code full of type-checking conditionals. The interviewer is watching for whether you can design systems where adding a new type doesn't require changing existing code.

The Problem: Type-Checking Conditionals

Here's a pattern that shows up constantly in weak LLD answers:

// BAD: ParkingLot checks vehicle type with conditionals
class ParkingLot {
    public ParkingSpot findSpot(Vehicle vehicle) {
        if (vehicle.type.equals("car")) {
            return findSpotOfSize(SpotSize.MEDIUM);
        } else if (vehicle.type.equals("motorcycle")) {
            return findSpotOfSize(SpotSize.SMALL);
        } else if (vehicle.type.equals("bus")) {
            return findSpotOfSize(SpotSize.LARGE);
        }
        throw new IllegalArgumentException("Unknown vehicle type");
    }
}
// BAD: C++ equivalent
class ParkingLot {
public:
    ParkingSpot* findSpot(const Vehicle& vehicle) {
        if (vehicle.type == "car") {
            return findSpotOfSize(SpotSize::MEDIUM);
        } else if (vehicle.type == "motorcycle") {
            return findSpotOfSize(SpotSize::SMALL);
        } else if (vehicle.type == "bus") {
            return findSpotOfSize(SpotSize::LARGE);
        }
        throw std::invalid_argument("Unknown vehicle type");
    }
};
# BAD: Python equivalent
class ParkingLot:
    def find_spot(self, vehicle) -> ParkingSpot:
        if vehicle.type == "car":
            return self._find_spot_of_size(SpotSize.MEDIUM)
        elif vehicle.type == "motorcycle":
            return self._find_spot_of_size(SpotSize.SMALL)
        elif vehicle.type == "bus":
            return self._find_spot_of_size(SpotSize.LARGE)
        raise ValueError("Unknown vehicle type")

This breaks every time you add a new vehicle type. Want to add a Truck? You have to open ParkingLot and modify its code. Want to add a Van? Same thing. The parking lot is doing work that belongs to the vehicles themselves.

The Fix: Let Each Type Define Its Own Behavior

Instead of asking "what type are you?" and branching, define a contract that every vehicle must fulfill:

// GOOD: Vehicle interface defines the contract
interface Vehicle {
    SpotSize getRequiredSpotSize();
}

class Car implements Vehicle {
    @Override
    public SpotSize getRequiredSpotSize() {
        return SpotSize.MEDIUM;
    }
}

class Motorcycle implements Vehicle {
    @Override
    public SpotSize getRequiredSpotSize() {
        return SpotSize.SMALL;
    }
}

class Truck implements Vehicle {
    @Override
    public SpotSize getRequiredSpotSize() {
        return SpotSize.LARGE;
    }
}

// ParkingLot never checks types. It never changes when you add new vehicles.
class ParkingLot {
    public ParkingSpot findSpot(Vehicle vehicle) {
        return findSpotOfSize(vehicle.getRequiredSpotSize());
    }
}
// GOOD: C++ uses abstract base class as interface
class Vehicle {
public:
    virtual ~Vehicle() = default;
    virtual SpotSize getRequiredSpotSize() const = 0;
};

class Car : public Vehicle {
public:
    SpotSize getRequiredSpotSize() const override { return SpotSize::MEDIUM; }
};

class Motorcycle : public Vehicle {
public:
    SpotSize getRequiredSpotSize() const override { return SpotSize::SMALL; }
};

class Truck : public Vehicle {
public:
    SpotSize getRequiredSpotSize() const override { return SpotSize::LARGE; }
};

class ParkingLot {
public:
    ParkingSpot* findSpot(const Vehicle& vehicle) {
        return findSpotOfSize(vehicle.getRequiredSpotSize());
    }
};
# GOOD: Python uses ABC (Abstract Base Class)
from abc import ABC, abstractmethod

class Vehicle(ABC):
    @abstractmethod
    def get_required_spot_size(self) -> SpotSize:
        pass

class Car(Vehicle):
    def get_required_spot_size(self) -> SpotSize:
        return SpotSize.MEDIUM

class Motorcycle(Vehicle):
    def get_required_spot_size(self) -> SpotSize:
        return SpotSize.SMALL

class Truck(Vehicle):
    def get_required_spot_size(self) -> SpotSize:
        return SpotSize.LARGE

class ParkingLot:
    def find_spot(self, vehicle: Vehicle) -> ParkingSpot:
        return self._find_spot_of_size(vehicle.get_required_spot_size())

Adding a Van now means creating one new class. ParkingLot never changes. No existing code is modified.

Interview tip: When you catch yourself writing if (thing.type == ...), stop and ask: "Should each type define this behavior itself?" Almost always, the answer is yes.

Interfaces Define the Contract

An interface says what something can do without specifying how. It's a contract: any class that implements the interface guarantees it has those methods.

// The contract: any payment method can process a payment
interface PaymentMethod {
    boolean process(long amountCents);
}

class CreditCardPayment implements PaymentMethod {
    private String cardNumber;

    public CreditCardPayment(String cardNumber) {
        this.cardNumber = cardNumber;
    }

    @Override
    public boolean process(long amountCents) {
        // Charge the credit card via payment gateway
        return gateway.charge(cardNumber, amountCents);
    }
}

class WalletPayment implements PaymentMethod {
    private long balanceCents;

    @Override
    public boolean process(long amountCents) {
        if (amountCents > balanceCents) return false;
        balanceCents -= amountCents;
        return true;
    }
}
class PaymentMethod {
public:
    virtual ~PaymentMethod() = default;
    virtual bool process(long amountCents) = 0;
};

class CreditCardPayment : public PaymentMethod {
    std::string cardNumber;
public:
    CreditCardPayment(std::string cardNumber) : cardNumber(std::move(cardNumber)) {}

    bool process(long amountCents) override {
        return gateway.charge(cardNumber, amountCents);
    }
};

class WalletPayment : public PaymentMethod {
    long balanceCents;
public:
    bool process(long amountCents) override {
        if (amountCents > balanceCents) return false;
        balanceCents -= amountCents;
        return true;
    }
};
from abc import ABC, abstractmethod

class PaymentMethod(ABC):
    @abstractmethod
    def process(self, amount_cents: int) -> bool:
        pass

class CreditCardPayment(PaymentMethod):
    def __init__(self, card_number: str):
        self._card_number = card_number

    def process(self, amount_cents: int) -> bool:
        return self._gateway.charge(self._card_number, amount_cents)

class WalletPayment(PaymentMethod):
    def __init__(self, balance_cents: int):
        self._balance_cents = balance_cents

    def process(self, amount_cents: int) -> bool:
        if amount_cents > self._balance_cents:
            return False
        self._balance_cents -= amount_cents
        return True

The key insight: CreditCardPayment talks to an external gateway. WalletPayment deducts from an internal balance. Completely different implementations, same interface. The calling code doesn't know or care which one it's using.

Runtime Polymorphism in Action

The caller works with the interface type, not the concrete class. The correct implementation is selected at runtime.

// The caller has no idea which PaymentMethod it's working with
PaymentMethod method = new CreditCardPayment("4111-1111-1111-1111");
method.process(5000);  // Calls CreditCardPayment.process()

method = new WalletPayment(10000);
method.process(5000);  // Calls WalletPayment.process()
std::unique_ptr<PaymentMethod> method = std::make_unique<CreditCardPayment>("4111-1111-1111-1111");
method->process(5000);  // Calls CreditCardPayment::process()

method = std::make_unique<WalletPayment>(10000);
method->process(5000);  // Calls WalletPayment::process()
method: PaymentMethod = CreditCardPayment("4111-1111-1111-1111")
method.process(5000)  # Calls CreditCardPayment.process()

method = WalletPayment(10000)
method.process(5000)  # Calls WalletPayment.process()

This is what makes polymorphism powerful in real systems. A Checkout class can accept any PaymentMethod. It processes the payment without knowing whether it's a credit card, wallet, bank transfer, or something that doesn't exist yet.

Interview tip: In your LLD answer, when you declare a variable, use the interface type, not the concrete type. Writing PaymentMethod method = ... instead of CreditCardPayment method = ... signals to the interviewer that you understand polymorphism.

When Polymorphism Helps vs. Hurts

Polymorphism is not always the right tool.

Use it when behavior varies by type and new types are likely. The parking lot example is a clear case: different vehicles need different spot sizes, and new vehicle types will be added.

Avoid it when you have only two or three types that will never change, or when adding layers of abstraction makes the code harder to trace without providing real extensibility. If you have a NotificationService that only ever sends emails and will never send anything else, wrapping it in a Notifier interface adds indirection for no benefit.

The test: will someone need to add a new implementation without modifying the existing code? If yes, use an interface. If no, a direct implementation is simpler and easier to debug.

The Connection to Design Patterns

Almost every design pattern in LLD interviews is built on polymorphism:

  • Strategy: swap algorithms at runtime (e.g., different PricingStrategy implementations)
  • Observer: notify a list of EventListener objects without knowing their concrete types
  • Factory: return different concrete classes through a common interface

You cannot implement any of these without interfaces and polymorphism. The patterns in the coming chapters will build directly on what you've learned here.

Quick Recap

Concept What it means Why it matters in interviews
Polymorphism Same method call, different behavior depending on the concrete type Eliminates if/else chains on types, makes code extensible
Interface Contract defining what methods a class must implement Decouples the caller from the implementation
Runtime dispatch The JVM/runtime picks the correct method at call time Callers work with abstractions, not concrete classes
When to use Behavior varies by type, new types are expected Shows you can design extensible systems
When to avoid Few types, no expected extensions Shows you don't over-engineer