Compile Ready
All low level design problems
Low Level Design/Concepts & Foundations/ Concept

OOP Fundamentals

Encapsulation, abstraction, inheritance, and polymorphism — the four pillars every LLD answer is built on.

6m readOOPEncapsulationAbstractionInheritancePolymorphism

Introduction

Object-oriented programming is the grammar of LLD interviews. Before patterns, UML, or system-specific classes, interviewers want to see whether you can place data and behavior together, hide unsafe state, expose small abstractions, and extend behavior without editing every caller.

For Amazon, Microsoft, Google, and Adobe style LLD rounds, the four pillars are not theory trivia. They are how you turn requirements into clean Java classes: encapsulation protects invariants, abstraction defines stable contracts, inheritance models true is-a relationships, and polymorphism lets callers depend on behavior rather than concrete types.

Learning Objectives

  • Explain encapsulation, abstraction, inheritance, and polymorphism with Java examples.

  • Choose inheritance or composition based on is-a versus has-a relationships.

  • Use access modifiers to protect invariants without overexposing implementation details.

  • Compare abstract classes and interfaces in interview-grade class design.

  • Distinguish compile-time polymorphism through overloading from runtime polymorphism through overriding.

  • Map OOP pillars directly to clean LLD entities, services, policies, and extension seams.

Core Theory

OOP is the foundation of every LLD answer

In LLD, you are not just listing classes. You are assigning responsibilities. A good class owns the state it can protect, exposes behavior that matches the domain, and depends on contracts where behavior may vary.

A useful interview loop is:

  • Find domain nouns and verbs.
  • Group state with the behavior that changes or validates it.
  • Hide fields unless callers truly need them.
  • Create interfaces or abstract bases only when you have more than one behavior or a clear future variation.
  • Prefer composition for ownership and collaboration; use inheritance only for true subtype identity.

Encapsulation means protecting invariants

Encapsulation is bundling data with the methods that keep it valid. The point is not merely making fields private; the point is preventing callers from creating impossible states.

In an LLD answer, a ParkingSpot should decide whether it can accept a vehicle. A BankAccount should decide whether a withdrawal is valid. A Cart should decide how quantities change. If a controller directly mutates fields, your invariants are scattered and hard to defend.

public final class BankAccount {
    private final String accountId;
    private long balanceInCents;

    public BankAccount(String accountId) {
        if (accountId == null || accountId.isBlank()) {
            throw new IllegalArgumentException("accountId is required");
        }
        this.accountId = accountId;
    }

    public long balanceInCents() {
        return balanceInCents;
    }

    public void deposit(long amountInCents) {
        if (amountInCents <= 0) {
            throw new IllegalArgumentException("deposit must be positive");
        }
        balanceInCents += amountInCents;
    }

    public void withdraw(long amountInCents) {
        if (amountInCents <= 0 || amountInCents > balanceInCents) {
            throw new IllegalArgumentException("invalid withdrawal");
        }
        balanceInCents -= amountInCents;
    }
}

Abstraction means exposing the behavior callers need

Abstraction hides how something is done and keeps what callers depend on small. In Java, you usually express abstraction with an interface, an abstract class, or a public API on a concrete class.

The interview signal is restraint. Do not create an interface for every class by reflex. Create one when there are multiple implementations, when policy may change, or when a high-level class should not know a low-level detail.

public interface PaymentMethod {
    PaymentReceipt pay(Money amount);
}

public final class CreditCardPayment implements PaymentMethod {
    @Override
    public PaymentReceipt pay(Money amount) {
        return new PaymentReceipt("card", amount);
    }
}

public final class CheckoutService {
    public PaymentReceipt checkout(Cart cart, PaymentMethod paymentMethod) {
        return paymentMethod.pay(cart.total());
    }
}

Inheritance models a true is-a relationship

Inheritance lets a subtype reuse and specialize behavior from a parent. Use it when every child is safely substitutable anywhere the parent is expected. This is the is-a test.

A Car is-a Vehicle. A Bus is-a Vehicle. A ParkingLot is not a VehicleManager child; it has levels, spots, and policies. If the relationship is role, ownership, or configuration, composition is usually cleaner.

public abstract class Vehicle {
    private final String plate;

    protected Vehicle(String plate) {
        this.plate = plate;
    }

    public String plate() {
        return plate;
    }

    public abstract int requiredSpots();
}

public final class Car extends Vehicle {
    public Car(String plate) {
        super(plate);
    }

    @Override
    public int requiredSpots() {
        return 1;
    }
}

Polymorphism lets one caller work with many implementations

Polymorphism means the same call can behave differently depending on the type involved.

Compile-time polymorphism is method overloading: the compiler chooses a method by parameter list. It is convenient for APIs but it does not replace a design seam.

Runtime polymorphism is method overriding: the object decides which implementation runs. This is what enables Strategy, State, Template Method, and most clean LLD extension points.

public final class NotificationService {
    public void send(String message) {
        send(message, Priority.NORMAL);
    }

    public void send(String message, Priority priority) {
        System.out.println(priority + ": " + message);
    }
}

public interface PricingPolicy {
    long priceInCents(Ticket ticket);
}

public final class HourlyPricingPolicy implements PricingPolicy {
    @Override
    public long priceInCents(Ticket ticket) {
        return ticket.hoursRoundedUp() * 200;
    }
}

Access modifiers are design tools

Access modifiers define your class boundary. In interviews, explain them as a way to protect invariants and reduce coupling, not as syntax trivia.

  • private for fields and helper methods that should never be called from outside.
  • package-private for collaborators inside the same package when no public API is needed.
  • protected sparingly for inheritance hooks; it widens the surface area for subclasses.
  • public only for stable behavior you are willing to support.

A strong Java LLD answer usually has private fields, public behavior methods, package-private helper classes where useful, and very few protected members.

public class Order {
    private final List<OrderLine> lines = new ArrayList<>();

    public void addItem(Product product, int quantity) {
        if (quantity <= 0) {
            throw new IllegalArgumentException("quantity must be positive");
        }
        lines.add(new OrderLine(product, quantity));
    }

    public List<OrderLine> lines() {
        return List.copyOf(lines);
    }
}

Abstract classes versus interfaces

Use an abstract class when related subclasses share state, constructor validation, or a partial algorithm. Use an interface when you want a capability or contract that unrelated classes can implement.

An abstract Vehicle can hold plate and size because every vehicle has that identity. A PricingPolicy interface is better because hourly pricing, surge pricing, and coupon pricing are strategies, not members of one family with shared state.

public abstract class Vehicle {
    private final String plate;

    protected Vehicle(String plate) {
        this.plate = plate;
    }

    public String plate() {
        return plate;
    }

    public abstract VehicleSize size();
}

public interface FeeCalculator {
    long calculateFee(Ticket ticket);
}

Composition models has-a relationships

Composition means one object owns or uses another object. It is the default choice for LLD because it keeps behavior swappable and avoids deep class hierarchies.

A ParkingLot has Levels. A Level has ParkingSpots. A CheckoutService has or receives a PaymentMethod. These are has-a relationships. If you model them with inheritance, you force unrelated concepts into one hierarchy and make extension harder.

public final class ParkingLot {
    private final List<Level> levels;
    private final PricingPolicy pricingPolicy;

    public ParkingLot(List<Level> levels, PricingPolicy pricingPolicy) {
        this.levels = List.copyOf(levels);
        this.pricingPolicy = pricingPolicy;
    }

    public long feeFor(Ticket ticket) {
        return pricingPolicy.priceInCents(ticket);
    }
}

Mapping pillars to interview classes

When you present an LLD solution, tie every class decision back to an OOP pillar:

  • Encapsulation: this class owns this state and enforces this invariant.
  • Abstraction: this interface hides a policy or external dependency.
  • Inheritance: these subclasses are true specializations of the parent.
  • Polymorphism: the caller uses the contract and the concrete object supplies behavior.

This makes your design sound intentional. Instead of saying I added a class, say the Cart owns line-item mutations, the PricingPolicy abstracts discount logic, and runtime polymorphism lets new pricing rules be added without touching CheckoutService.

Diagrams

Inheritance and runtime polymorphism

Rendering diagram…
Kennel stores Animal references and calls speak through the abstract type. Dog and Cat provide the runtime behavior, while SoundMaker is a replaceable capability.

Comparisons

Abstract class vs interface

DimensionAbstract classInterfaceInterview guidance
Primary purposeShare common state or a partial base algorithmDefine a capability or contractUse the narrowest abstraction that captures variation
StateCan hold instance fields and constructor validationShould not hold per-object mutable statePut identity and shared validation in an abstract class only when all children need it
Inheritance limitA class can extend only one abstract classA class can implement many interfacesPrefer interfaces for cross-cutting capabilities such as payable, searchable, or notifiable
Best LLD exampleVehicle base class with plate and sizePricingPolicy or PaymentMethod contractAbstract class for family identity; interface for swappable behavior

Inheritance vs composition

QuestionInheritanceCompositionDecision rule
RelationshipIs-aHas-a or uses-aIf the sentence is not naturally is-a, do not extend
Change impactParent changes can affect all childrenCollaborators can be swapped independentlyComposition is safer when behavior changes frequently
Interview signalGood for stable taxonomiesGood for policies, ownership, and dependenciesDefault to composition unless subtype substitutability is obvious
ExampleCar extends VehicleParkingLot has PricingPolicyUse inheritance for identity and composition for collaboration

Java access modifiers in LLD

ModifierVisible fromTypical LLD use
privateOnly the declaring classFields, helper methods, and invariant-preserving state
package-privateClasses in the same packageInternal helpers that should not become public API
protectedSubclasses and same packageRare extension hooks in abstract base classes
publicEvery callerStable behavior that represents the class contract

Best Practices

  • Start with responsibilities, not fields. A class should own behavior, not just data.

  • Keep fields private and expose intention-revealing methods such as reserve, cancel, pay, or assign.

  • Use interfaces for policies that vary, such as pricing, matching, routing, notification, or payment.

  • Use abstract classes only when subclasses share identity, state, validation, or a fixed algorithm skeleton.

  • Prefer composition for services and owned parts. Deep inheritance hierarchies are hard to explain and harder to change.

  • When introducing polymorphism, show the caller depending on the parent type or interface.

  • Name invariants explicitly in the interview, such as one vehicle per spot or balance cannot go negative.

  • Return immutable views or copies instead of exposing mutable internal collections.

Common Mistakes

  • ×

    Using inheritance because two classes share fields, even when there is no true is-a relationship.

  • ×

    Creating an interface for every class without a second implementation or clear variation point.

  • ×

    Putting validation in controllers instead of inside the domain object that owns the state.

  • ×

    Making fields public or returning mutable lists, allowing callers to bypass invariants.

  • ×

    Confusing overloading with runtime polymorphism and claiming it enables open-ended extension.

  • ×

    Using protected fields as a shortcut, which lets subclasses corrupt parent state.

  • ×

    Building one god class that knows every rule instead of assigning behavior to domain objects and policies.

  • ×

    Choosing inheritance for pricing, payment, or allocation policies when composition with an interface would be cleaner.

Quiz

0/7 answered

  1. 1.What is the strongest reason to keep fields private in an LLD class?

  2. 2.Which relationship is the best fit for inheritance?

  3. 3.What is runtime polymorphism in Java?

  4. 4.When is an interface most useful in an LLD answer?

  5. 5.Which statement best describes abstraction?

  6. 6.Why is composition often preferred over inheritance for policies?

  7. 7.What is compile-time polymorphism in Java?

Flashcards

Cheat Sheet

Four pillars: Encapsulation protects state; abstraction exposes a small contract; inheritance models true is-a subtype identity; polymorphism lets one caller work with many implementations.

Encapsulation in LLD: Keep fields private. Put validation and state changes inside the class that owns the invariant.

Abstraction in LLD: Use interfaces for variable policies and external dependencies. Keep contracts small.

Inheritance rule: Use only when the child can safely replace the parent. Car is-a Vehicle is valid; ParkingLot has Levels is composition.

Polymorphism: Overloading is compile-time convenience. Overriding is runtime extension and powers most design patterns.

Access modifiers: Private by default, public for stable behavior, package-private for internal helpers, protected only for deliberate subclass hooks.

Interview framing: For every class, say what it owns, what invariant it protects, what contract it exposes, and what variation point it keeps open.

References

  • BookEffective JavaJoshua Bloch
  • BookHead First Object-Oriented Analysis and DesignBrett McLaughlin, Gary Pollice, and David West
  • DocsOracle Java Tutorials: Object-Oriented Programming Concepts
  • BookDesign Patterns: Elements of Reusable Object-Oriented SoftwareErich Gamma, Richard Helm, Ralph Johnson, and John Vlissides