Back to LLD Concepts
Programming Paradigm

Object-Oriented Programming

A complete guide to OOP — from first principles (classes, objects) through the four pillars, to advanced topics like generics, composition, descriptors, and metaclasses. Every concept shown in Java, Python, and JavaScript.

What is OOP?

Object-Oriented Programming is a programming paradigm that organises software around objects — bundles of state (data) and behaviour (methods) — rather than around functions and logic alone.

OOP rests on four traditional pillars — Encapsulation, Inheritance, Polymorphism, and Abstraction — plus a set of advanced techniques that build on top of them.

📦 Classes & Objects

The basic building blocks — blueprints and instances

🔒 Encapsulation

Bundle state + behaviour, hide implementation details

🌱 Inheritance

Reuse and specialise existing classes

🎭 Polymorphism

Same interface, different behaviour at runtime

🧩 Abstraction

Expose what, hide how — simplify interfaces

🔀 Composition

"Has-a" over "is-a" — flexible object assembly

⚡ Advanced OOP

Generics, descriptors, metaclasses, protocols

Concept 1 — Beginner

Classes & Objects

"A class is a blueprint. An object is a specific thing built from that blueprint."

The Core Idea

Before OOP, programs were collections of procedures operating on raw data. OOP bundles the data (fields/attributes) and the operations on that data (methods) together into one unit — the object.

A class defines the structure. An object (instance) is a specific realisation with its own independent state.

Fields (State)

Variables stored inside the object — each instance has its own copy.

Methods (Behaviour)

Functions defined inside the class — operate on the object's own fields.

Constructor

Special method called when the object is created. Sets up initial state.

Class vs Instance: key distinctions

AspectClassInstance (Object)
What it isBlueprint / TemplateReal thing in memory
CountOne per definitionUnlimited instances
StateNo runtime stateHas its own state
Created withclass keywordnew / () call

Code Example

// ── Classes & Objects ─────────────────────────────────────────
public class BankAccount {
    private String accountId;
    private String holderName;
    private double balance;
    private final String currency;

    public BankAccount(String accountId, String holderName, String currency) {
        this.accountId  = accountId;
        this.holderName = holderName;
        this.currency   = currency;
        this.balance    = 0.0;
    }

    public void deposit(double amount) {
        if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");
        this.balance += amount;
        System.out.printf("[%s] Deposited %.2f. Balance: %.2f %s%n",
                          accountId, amount, balance, currency);
    }

    public void withdraw(double amount) {
        if (amount <= 0)           throw new IllegalArgumentException("Amount must be positive");
        if (amount > this.balance) throw new IllegalStateException("Insufficient funds");
        this.balance -= amount;
    }

    public double getBalance()   { return balance; }
    public String getAccountId() { return accountId; }

    @Override public String toString() {
        return String.format("BankAccount{id='%s', holder='%s', balance=%.2f %s}",
                             accountId, holderName, balance, currency);
    }
    @Override public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof BankAccount)) return false;
        return accountId.equals(((BankAccount) o).accountId);
    }
    @Override public int hashCode() { return accountId.hashCode(); }
}

public class Main {
    public static void main(String[] args) {
        BankAccount alice = new BankAccount("ACC-001", "Alice", "INR");
        BankAccount bob   = new BankAccount("ACC-002", "Bob",   "INR");
        alice.deposit(50_000);
        alice.withdraw(12_000);
        bob.deposit(20_000);
        System.out.println(alice);
        System.out.println(alice.equals(bob));  // false
    }
}

Real-World Examples

User in Django / Rails

User.objects.create(email=...) creates an instance from the User class. The class defines the schema; each user record is an instance with its own data.

React Component

A React class component is an OOP class. Its state is instance-level; its render() and lifecycle methods are the behaviour.

Java Thread

new Thread(runnable) creates an object from the Thread class. Each Thread manages its own execution stack and state independently.

MongoDB Document Model

Mongoose models are classes. Schema defines fields + validations. new User({ name: "Alice" }) creates an instance.

Concept 2 — Beginner

Encapsulation

"Bundle data and the methods that operate on it together, and restrict direct access to the internal state."

The Two Parts of Encapsulation

1. Bundling

State and methods that belong together are in one class. UserAccount holds passwordHash AND login() — they're co-located because they're co-dependent.

2. Information Hiding

Internal details are private. Outside code accesses state only through controlled public methods. passwordHash is never exposed — only login() can compare it.

Access Modifiers Compared

LevelJavaPythonJavaScriptAccessible from
PublicpublicnamenameEveryone
Protectedprotected_name_name (convention)Class + subclasses
Privateprivate__name#nameClass only

Why not make everything public? Public state lets external code set account.balance = -999 directly, bypassing all validation. Private fields force all changes through methods that enforce business rules.

Code Example

// ── Encapsulation ─────────────────────────────────────────────
public class UserAccount {
    private String  email;
    private String  passwordHash;
    private boolean verified;
    private int     failedLoginAttempts;
    private static final int MAX_ATTEMPTS = 5;

    public UserAccount(String email, String rawPassword) {
        setEmail(email);
        this.passwordHash = hash(rawPassword);
        this.verified     = false;
        this.failedLoginAttempts = 0;
    }

    public String getEmail()    { return email; }
    public boolean isVerified() { return verified; }
    public boolean isLocked()   { return failedLoginAttempts >= MAX_ATTEMPTS; }

    public void setEmail(String email) {
        if (email == null || !email.contains("@"))
            throw new IllegalArgumentException("Invalid email: " + email);
        this.email = email.toLowerCase().trim();
    }

    public boolean login(String rawPassword) {
        if (isLocked()) throw new IllegalStateException("Account locked");
        if (hash(rawPassword).equals(passwordHash)) {
            failedLoginAttempts = 0;
            return true;
        }
        failedLoginAttempts++;
        return false;
    }

    public void verify() { this.verified = true; }
    private String hash(String raw) { return Integer.toHexString(raw.hashCode()); }
}

Benefits

  • Object stays in a valid state — invariants guaranteed
  • Internal representation can change without breaking callers
  • Bugs are localised — only the class can corrupt its own state
  • API is clear — only public methods are the contract

Common Pitfalls

  • Getters/setters for every field = no real encapsulation
  • Returning mutable objects from getters — callers mutate internal state
  • Making everything public "for simplicity" — loses all protection
  • Over-encapsulating simple value objects that need no protection
Concept 3 — Beginner

Inheritance

"A subclass inherits the fields and methods of its parent, then extends or overrides them to specialise behaviour."

How Inheritance Works

When class B extends A, B automatically gets all of A's non-private fields and methods. B can then use, override, extend, or add new methods.

Inheritance creates an IS-A relationship: ElectricCar IS-A Vehicle. Anywhere you use a Vehicle, you can use an ElectricCar — the foundation of polymorphism.

What Subclasses Can Do

  • InheritUse parent methods without any change
  • OverrideReplace parent method with a new implementation
  • ExtendCall super() then add new logic
  • AddDefine entirely new methods not in parent

Java vs Python vs JS Differences

  • Java:Single inheritance only. @Override annotation. final prevents overriding.
  • Python:Multiple inheritance supported. MRO (C3 linearisation) resolves conflicts.
  • JS:Prototype chain. Only single extends. super() mandatory before this in constructors.

⚠️ Inheritance pitfall — Fragile Base Class: Changes to a parent class can unexpectedly break all subclasses. This is why "favour composition over inheritance" is the modern advice.

Code Example

// ── Inheritance ───────────────────────────────────────────────
public class Vehicle {
    protected String make, model;
    protected int    year;
    protected double fuelLevel = 1.0;

    public Vehicle(String make, String model, int year) {
        this.make = make; this.model = model; this.year = year;
    }
    public void startEngine() { System.out.println(make + " " + model + " engine started."); }
    public void refuel(double amount) { fuelLevel = Math.min(1.0, fuelLevel + amount); }
    public String getInfo() { return year + " " + make + " " + model; }
}

public class ElectricCar extends Vehicle {
    private double batteryLevel = 1.0;
    private int    rangeKm;

    public ElectricCar(String make, String model, int year, int rangeKm) {
        super(make, model, year);
        this.rangeKm = rangeKm;
    }

    @Override public void startEngine() {
        System.out.println(make + " " + model + " motors activated silently. ⚡");
    }
    @Override public void refuel(double amount) {
        batteryLevel = Math.min(1.0, batteryLevel + amount);
        System.out.printf("Charged to %.0f%%. Range: ~%d km%n",
                          batteryLevel * 100, (int)(batteryLevel * rangeKm));
    }
    public int estimatedRange() { return (int)(batteryLevel * rangeKm); }

    @Override public String getInfo() {
        return super.getInfo() + " [Electric, " + rangeKm + "km range]";
    }
}

Vehicle car = new ElectricCar("Tesla", "Model 3", 2024, 500);
car.startEngine();
car.refuel(0.5);
System.out.println(car.getInfo());
System.out.println(car instanceof Vehicle); // true

Real-World Examples

Java Exception Hierarchy

Throwable ← Exception ← RuntimeException ← IllegalArgumentException. Each level adds specificity.

Python's io Module

IOBase ← RawIOBase ← FileIO. Each subclass specialises for a different kind of I/O while inheriting the core protocol.

React Component (Class API)

class MyComponent extends React.Component. Inherits render lifecycle, setState, and all React hooks.

Django Model Inheritance

class Customer(User) adds customer-specific fields to the built-in User. All User methods work unchanged.

Concept 4 — Intermediate

Polymorphism

"One interface, many implementations — the same method call produces different behaviour depending on the actual object type at runtime."

Types of Polymorphism

Subtype (Runtime)

Dynamic dispatch, method overriding

A variable of type Shape holds a Circle. Calling shape.area() dispatches to Circle.area() at runtime.

Shape s = new Circle(); s.area()
Ad-hoc (Compile-time)

Method overloading

Same method name, different parameter types. The compiler picks the right version based on argument types.

add(int,int) vs add(double,double)
Parametric

Generics / templates

A single generic class works with any type. List<Integer>, List<String> — same implementation, different type parameters.

List<T>, Result<T>, Optional<T>

Duck Typing (Python & JavaScript)

Python and JavaScript use structural polymorphism — if an object has the right method, it works, regardless of its class hierarchy.

# Any object with a describe() method works — no base class needed
def print_description(thing):
    thing.describe()   # polymorphic — resolved at runtime

print_description(Circle("red", 5))   # calls Circle.describe()
print_description(Star())             # Star doesn't extend Shape — doesn't matter!

Code Example

// ── Polymorphism ──────────────────────────────────────────────
abstract class Shape {
    protected String colour;
    public Shape(String colour) { this.colour = colour; }
    public abstract double area();
    public abstract double perimeter();
    public void describe() {
        System.out.printf("%s %s — area=%.2f, perimeter=%.2f%n",
                colour, getClass().getSimpleName(), area(), perimeter());
    }
}
class Circle extends Shape {
    private final double radius;
    public Circle(String colour, double radius) { super(colour); this.radius = radius; }
    @Override public double area()      { return Math.PI * radius * radius; }
    @Override public double perimeter() { return 2 * Math.PI * radius; }
}
class Rectangle extends Shape {
    private final double width, height;
    public Rectangle(String c, double w, double h) { super(c); width=w; height=h; }
    @Override public double area()      { return width * height; }
    @Override public double perimeter() { return 2 * (width + height); }
}

// Same call — different behaviour at runtime
List<Shape> canvas = List.of(new Circle("Red",5), new Rectangle("Blue",4,6));
double totalArea = 0;
for (Shape shape : canvas) {
    shape.describe();        // polymorphic dispatch
    totalArea += shape.area();
}
System.out.printf("Total area: %.2f%n", totalArea);

Why Polymorphism Matters

  • Write once, run for any type — no if-else per type
  • New types can be added without changing existing code (OCP)
  • Code is shorter, more expressive, and easier to maintain
  • The calling code doesn't need to know concrete types

Real-World Examples

  • Collections.sort(): Works for any List of Comparable — String, Integer, LocalDate
  • React render(): Every component has render — React calls it polymorphically for the whole tree
  • Python print(): Calls __str__ on anything — your class gets formatting for free
  • SQL drivers: execute() works on MySQL, Postgres, SQLite connections identically
Concept 5 — Intermediate

Abstraction

"Show only what's necessary. Hide the complexity behind a clean, simple interface."

Abstraction vs Encapsulation

Encapsulation (HOW)

Hides internals of a class using access modifiers. The hiding is about state and implementation details within a class.

Abstraction (WHAT)

Hides entire implementations behind an interface. You only know the contract — whether it's Stripe or Razorpay underneath is invisible.

Levels of Abstraction

Methodlist.sort() — you don't know it's Timsort
ClassFile — you call read(), not syscall open/mmap
InterfacePaymentGateway — Stripe/Razorpay hidden behind contract
ModuleHTTP client — TCP/IP hidden behind requests.get()
ServiceS3 API — distributed storage hidden behind put_object()

Code Example

// ── Abstraction — Template Method Pattern ─────────────────────
public abstract class PaymentGateway {
    public final PaymentResult processPayment(PaymentRequest req) {
        validateRequest(req);
        PaymentResult result = charge(req);  // abstract step
        audit(req, result);
        return result;
    }
    protected abstract PaymentResult charge(PaymentRequest req);
    private void validateRequest(PaymentRequest req) {
        if (req.getAmount() <= 0) throw new IllegalArgumentException("Amount must be positive");
    }
    private void audit(PaymentRequest req, PaymentResult result) {
        System.out.printf("[AUDIT] %s | %.2f %s | %s%n",
                req.getGateway(), req.getAmount(), req.getCurrency(), result.getStatus());
    }
}
public class StripeGateway extends PaymentGateway {
    @Override
    protected PaymentResult charge(PaymentRequest req) {
        System.out.println("[Stripe] Charging via Stripe API...");
        return new PaymentResult("SUCCESS", "ch_stripe_" + System.nanoTime());
    }
}
// Client programs to the abstraction — doesn't know it's Stripe
PaymentGateway gateway = new StripeGateway();
gateway.processPayment(new PaymentRequest(999.00, "INR", "Stripe"));

Real-World Examples

JDBC (Java)

Connection, Statement, ResultSet abstract away every database vendor. Code written against JDBC runs unchanged on MySQL, Postgres, or SQLite.

Python's ABC module

ABCMeta and @abstractmethod enforce contracts. Subclasses that miss an abstract method raise TypeError at import time.

Express.js Request/Response

req and res abstract raw Node.js IncomingMessage. You call res.json() without knowing the HTTP wire format.

Cloud Storage SDK

AWS S3, GCS, Azure Blob all expose a similar put/get/delete abstraction. Infrastructure changes; your code doesn't.

Concept 6 — Intermediate

Interfaces vs Abstract Classes

"Interfaces define what a type can DO. Abstract classes define what a type IS, with some of the HOW already filled in."

When to Use Which

Use an Interface / Protocol when:

  • Defining a capability a class can have (Serialisable, Comparable)
  • Multiple unrelated classes need to share a contract
  • A class should satisfy multiple roles simultaneously
  • No shared implementation to provide

Use an Abstract Class when:

  • Sharing concrete code among closely related classes
  • You need a constructor or instance state in the base
  • Template Method pattern — algorithm skeleton belongs here
  • Strong IS-A relationship (Report, PaymentGateway)

Feature Comparison

FeatureInterface / ProtocolAbstract Class
Multiple inheritance✓ implement many✗ extend one (Java/JS)
Constructor
Instance fields✗ (only constants)
Relationship"can do" (CAN-DO)"is a kind of" (IS-A)

Code Example

// ── Interface vs Abstract Class ────────────────────────────────
// INTERFACE — pure contract, implement many
public interface Serialisable {
    byte[] serialise();
    default String toBase64() {
        return java.util.Base64.getEncoder().encodeToString(serialise());
    }
}
public interface Auditable   { String getAuditId(); }
public interface Publishable { void publish(String topic); }

// One class, many roles
public class OrderEvent implements Serialisable, Auditable, Publishable {
    private final String orderId;
    public OrderEvent(String orderId) { this.orderId = orderId; }
    @Override public byte[]  serialise()    { return orderId.getBytes(); }
    @Override public String  getAuditId()   { return orderId; }
    @Override public void    publish(String topic) { System.out.println("Published " + orderId + " → " + topic); }
}

// ABSTRACT CLASS — shared code + contract
public abstract class Report {
    protected final String title;
    protected Report(String title) { this.title = title; }
    public final String generate() {
        return buildHeader() + "\n" + buildBody() + "\n" + buildFooter();
    }
    private String buildHeader() { return "=== " + title.toUpperCase() + " ==="; }
    private String buildFooter() { return "=== End of Report ==="; }
    protected abstract String buildBody();
}
public class SalesReport extends Report {
    private final java.util.List<String> items;
    public SalesReport(java.util.List<String> items) { super("Sales Report"); this.items = items; }
    @Override protected String buildBody() { return String.join("\n", items); }
}
Concept 7 — Advanced

Composition vs Inheritance

"Favour object composition over class inheritance." — Gang of Four (Design Patterns, 1994)

The Problem with Deep Inheritance

Inheritance creates a compile-time binding between parent and child. It exposes parent internals to subclasses, creates the Fragile Base Class problem, and leads to deep hierarchies that are hard to reason about.

❌ Inheritance problems

  • Fragile Base Class — parent changes cascade to all children
  • Can't mix capabilities in single-inheritance languages
  • Behaviour locked at compile time — can't swap at runtime
  • Deep hierarchies become hard to understand

✅ Composition advantages

  • Mix any capabilities freely — duck swims AND flies
  • Swap behaviours at runtime (Strategy pattern)
  • No coupling to parent internals
  • Each capability object is independently testable

The rule of thumb: Use inheritance only for genuine IS-A relationships. For everything else — capabilities, behaviours, shared utilities — use composition. When in doubt, compose.

Code Example

// ── Composition vs Inheritance ────────────────────────────────
// Capabilities as independent interfaces
@FunctionalInterface interface FlyBehaviour  { void fly(); }
@FunctionalInterface interface SwimBehaviour { void swim(); }

class WingFlight       implements FlyBehaviour  { public void fly()  { System.out.println("Flying with wings! 🦅"); } }
class RocketPack       implements FlyBehaviour  { public void fly()  { System.out.println("ROCKET PROPULSION! 🚀"); } }
class NoFlight         implements FlyBehaviour  { public void fly()  { System.out.println("Can't fly."); } }
class BreaststrokeSwim implements SwimBehaviour { public void swim() { System.out.println("Swimming gracefully."); } }
class NoSwim           implements SwimBehaviour { public void swim() { System.out.println("Can't swim."); } }

class Animal {
    private final String name;
    private final FlyBehaviour  flyBehaviour;
    private final SwimBehaviour swimBehaviour;

    public Animal(String name, FlyBehaviour fly, SwimBehaviour swim) {
        this.name = name; flyBehaviour = fly; swimBehaviour = swim;
    }
    public void performActions() {
        System.out.print(name + ": "); flyBehaviour.fly();
        System.out.print(name + ": "); swimBehaviour.swim();
    }
}

Animal duck    = new Animal("Duck",    new WingFlight(),  new BreaststrokeSwim());
Animal penguin = new Animal("Penguin", new NoFlight(),    new BreaststrokeSwim());
Animal eagle   = new Animal("Eagle",   new WingFlight(),  new NoSwim());
duck.performActions();
penguin.performActions();
eagle.performActions();
Concept 8 — Advanced

Advanced OOP

"Generics, descriptors, metaclasses, proxies, and language-level object protocols — the mechanics that power every framework."

Generics / Type Parameters

Write one class that works safely with any type. Result<T> wraps any success value. The type parameter is checked at compile time.

Result<User>, Optional<String>

Builder Pattern

Fluent construction of complex objects step by step. Creates immutable objects with readable construction code.

HttpRequest.builder("POST", url).header(...).build()

Descriptors (Python)

__get__, __set__, __set_name__ — intercept attribute access at the class level. Power @property, @classmethod, and Django Field classes.

class Validated: def __set__(self, obj, val): ...

Metaclasses (Python)

Classes that control class creation. Custom metaclasses enforce patterns at definition time — Singleton, ORMs, ABC.

class SingletonMeta(type): def __call__(cls, ...)

Proxy Object (JS)

Intercept fundamental object operations (get, set, has). Powers Vue 3 reactivity and validation layers without modifying the target.

new Proxy(target, { set(obj, prop, val) { ... } })

Iterator Protocol

Any object that implements [Symbol.iterator] (JS) or __iter__/__next__ (Python) can be iterated with for loops and spread operators.

class Range { [Symbol.iterator]() { ... } }

Code Example

// ── Advanced OOP — Result type + Builder ──────────────────────
import java.util.function.*;

public class Result<T> {
    private final T value; private final String error; private final boolean success;
    private Result(T v, String e, boolean ok) { value=v; error=e; success=ok; }
    public static <T> Result<T> ok(T value)      { return new Result<>(value, null, true); }
    public static <T> Result<T> fail(String err) { return new Result<>(null, err, false); }
    public boolean isSuccess()  { return success; }
    public T       getValue()   { if (!success) throw new java.util.NoSuchElementException(); return value; }
    public <U> Result<U> map(Function<T,U> f)            { return success ? Result.ok(f.apply(value)) : Result.fail(error); }
    public <U> Result<U> flatMap(Function<T,Result<U>> f){ return success ? f.apply(value) : Result.fail(error); }
}

// Builder Pattern
public class HttpRequest {
    private final String method, url, body;
    private final java.util.Map<String,String> headers;
    private final int timeoutMs;
    private HttpRequest(Builder b) { method=b.method; url=b.url; headers=java.util.Collections.unmodifiableMap(b.headers); body=b.body; timeoutMs=b.timeoutMs; }
    public static Builder builder(String method, String url) { return new Builder(method, url); }
    public static class Builder {
        private final String method, url;
        private final java.util.Map<String,String> headers = new java.util.LinkedHashMap<>();
        private String body = ""; private int timeoutMs = 5000;
        private Builder(String method, String url) { this.method=method; this.url=url; }
        public Builder header(String k, String v) { headers.put(k,v); return this; }
        public Builder body(String b)  { body=b;      return this; }
        public Builder timeout(int ms) { timeoutMs=ms; return this; }
        public HttpRequest build()     { return new HttpRequest(this); }
    }
}

HttpRequest req = HttpRequest.builder("POST", "https://api.example.com/orders")
    .header("Content-Type","application/json")
    .header("Authorization","Bearer token123")
    .body("{\"item\":\"laptop\",\"qty\":1}")
    .timeout(3000)
    .build();

Where Advanced OOP Powers Real Frameworks

Django ORM Fields

Django's CharField, IntegerField are Python Descriptors. They intercept __set__ on model instances to validate data before it reaches the database.

Vue 3 Reactivity

Vue's reactive() wraps objects in a JavaScript Proxy. When you set state.count = 5, the Proxy's set trap fires, triggering DOM updates automatically.

Java Generics

ArrayList<User> uses generics for compile-time type safety. The JVM erases types at runtime (type erasure), but javac catches ClassCastException at compile time.

Python's @dataclass

A metaclass-like decorator that inspects class annotations at definition time and generates __init__, __repr__, __eq__ automatically.

OOP Concepts at a Glance

ConceptCore Question
Classes & Objects"What blueprint does this data and behaviour belong to?"
Encapsulation"Who is allowed to change this state?"
Inheritance"Is this a more specific version of the parent?"
Polymorphism"Can one call work for all types without if-else?"
Abstraction"Can the caller ignore the implementation details?"
Composition"Should I use HAS-A instead of IS-A here?"
Advanced OOP"Can a language feature handle this automatically?"

Take OOP Further with Design Patterns

Design patterns are proven OOP solutions to recurring design problems. Each pattern applies one or more OOP concepts to solve a specific structural challenge: