Back to OOPs Fundamentals
Q1Question 1 of 4

What are the four pillars of OOP?

Key Concepts
  • Encapsulation bundles data with methods and restricts direct access using private fields and public methods
  • Abstraction hides complex implementation details behind a simple interface so the caller knows what, not how
  • Inheritance lets a child class reuse fields and methods from a parent class, establishing an IS-A relationship
  • Polymorphism allows the same method call to behave differently depending on the actual object type at runtime
  • Together the four pillars enable modular, maintainable, and extensible systems like a banking platform
  • Encapsulation and abstraction often work together: encapsulation protects data, abstraction simplifies usage

Definition

Encapsulation protects data, abstraction hides complexity, inheritance reuses code, and polymorphism flexes behavior. Together they make code modular, maintainable, and extensible.

Object-Oriented Programming is built on four fundamental pillars.

  • Encapsulation bundles data (fields) and methods into a single class and restricts direct access using access modifiers (private fields + public methods).
  • Abstraction hides complex implementation details and exposes only the essential interface so the caller knows WHAT a method does, not HOW it does it internally.
  • Inheritance allows a child class to acquire the fields and methods of a parent class, promoting code reuse and establishing an IS-A relationship (SavingsAccount IS-A Account).
  • Polymorphism means many forms: the same method call behaves differently depending on the actual object type at runtime. account.calculateInterest() computes 4% for SavingsAccount and 0% for CurrentAccount, decided at runtime via dynamic dispatch.

Think of National Bank of India. Encapsulation: Rajesh's account balance is private and can only change through deposit() and withdraw() methods that validate every modification. Abstraction: Rajesh calls transfer(toAccount, amount) without knowing the 15 internal steps (debit, credit, audit, notification, compliance check). Inheritance: SavingsAccount and CurrentAccount both extend Account, inheriting common features like balance and deposit(). Polymorphism: the month-end batch calls account.calculateInterest() on each account, and the JVM dispatches to the correct override at runtime.

Understanding the Concepts

What are the four pillars of OOP?

Object-Oriented Programming is built on four pillars that work together to produce modular, maintainable, and extensible software. Each pillar addresses a different concern: encapsulation controls access, abstraction simplifies interfaces, inheritance promotes reuse, and polymorphism enables flexibility. In a banking system these four pillars appear in every class, every service, and every interaction.

What is encapsulation?

Encapsulation is the most fundamental pillar. It combines data and behavior into a class and controls access. The private fields cannot be accessed directly from outside the class. Only public getter and setter methods (or domain methods like deposit and withdraw) can read or modify the data. This is not just about hiding data: it is about controlling how data changes. A bank account balance should never be set to a negative value. Without encapsulation, any code could write account.balance = -5000. With encapsulation, withdraw(amount) validates the amount, checks minimum balance, and throws InsufficientFundsException if the balance would go below zero.

JAVA
package com.bank.core;

import java.math.BigDecimal;

public class Account {
    private BigDecimal balance;          // private: cannot be accessed directly
    private final String accountId;      // private + final: immutable after creation

    public Account(String accountId, BigDecimal initialBalance) {
        this.accountId = accountId;
        this.balance = initialBalance;
    }

    public void withdraw(BigDecimal amount) {
        if (amount.signum() <= 0) {
            throw new IllegalArgumentException("Amount must be positive");
        }
        if (balance.subtract(amount).compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalStateException("Balance insufficient for " + amount);
        }
        balance = balance.subtract(amount);
    }

    public BigDecimal getBalance() { return balance; }
    // No setBalance() method: balance can only change through deposit() or withdraw().
}

What is abstraction?

Abstraction hides the implementation details behind a clean interface. The caller interacts with what the system does, not how it does it. In Java, abstraction is achieved through abstract classes and interfaces. An interface PaymentService declares transfer(from, to, amount). The caller does not know whether it uses NEFT, RTGS, or UPI internally. The implementation can change without affecting callers.

JAVA
package com.bank.service;

import java.math.BigDecimal;

public interface PaymentService {
    TransferResult transfer(String fromAccount, String toAccount, BigDecimal amount);
}

public class NeftPaymentService implements PaymentService {
    @Override
    public TransferResult transfer(String from, String to, BigDecimal amount) {
        // 1. Validate accounts
        // 2. Check balance
        // 3. Debit source via RBI clearing
        // 4. Credit destination (next-day settlement)
        // 5. Generate UTR number
        // 6. Send SMS notification
        // 7. Write audit log
        // Caller sees NONE of this complexity.
        return new TransferResult("UTR12345", "SUCCESS");
    }
}

public class UpiPaymentService implements PaymentService {
    @Override
    public TransferResult transfer(String from, String to, BigDecimal amount) {
        // Completely different internals: VPA resolution, NPCI switch, instant credit.
        // But the caller code does not change.
        return new TransferResult("UPI98765", "SUCCESS");
    }
}

What is inheritance?

Inheritance establishes an IS-A relationship where a child class extends a parent class, inheriting all non-private fields and methods. The child can add new behavior and override existing behavior. In banking: SavingsAccount IS-A Account, CurrentAccount IS-A Account. Both share accountId, balance, and deposit(). SavingsAccount adds interestRate and calculateInterest(). CurrentAccount adds overdraftLimit and overrides withdraw() to allow overdraft.

JAVA
package com.bank.core;

import java.math.BigDecimal;

public class SavingsAccount extends Account {
    private BigDecimal interestRate;

    public SavingsAccount(String accountId, BigDecimal balance, BigDecimal rate) {
        super(accountId, balance);
        this.interestRate = rate;
    }

    public BigDecimal calculateInterest() {
        return getBalance().multiply(interestRate);
    }
}

public class CurrentAccount extends Account {
    private BigDecimal overdraftLimit;

    public CurrentAccount(String accountId, BigDecimal balance, BigDecimal limit) {
        super(accountId, balance);
        this.overdraftLimit = limit;
    }

    @Override
    public void withdraw(BigDecimal amount) {
        BigDecimal effective = getBalance().add(overdraftLimit);
        if (effective.compareTo(amount) < 0) {
            throw new IllegalStateException("Exceeds overdraft limit");
        }
        // CurrentAccount allows withdrawal up to balance + overdraft.
    }
}

What is polymorphism?

Polymorphism means the same method call executes different code depending on the actual object type at runtime. The variable type is the parent (Account), but the actual object is the child (SavingsAccount or CurrentAccount). When account.withdraw(amount) is called, the JVM looks at the actual object type and calls the correct version. This is called dynamic dispatch: the method resolution happens at runtime, not compile time.

JAVA
package com.bank.service;

import java.math.BigDecimal;
import java.util.List;

public class InterestBatchJob {

    public void processMonthEnd(List<Account> allAccounts) {
        for (Account account : allAccounts) {
            if (account instanceof SavingsAccount sa) {
                BigDecimal interest = sa.calculateInterest();
                account.deposit(interest);
            }
            // No need to know all possible account types at compile time.
        }
    }

    public void processWithdrawal(Account account, BigDecimal amount) {
        account.withdraw(amount);
        // If account is SavingsAccount: calls Account.withdraw() (strict balance check)
        // If account is CurrentAccount: calls CurrentAccount.withdraw() (allows overdraft)
        // Same code, different behavior at runtime.
    }
}

How do the four pillars work together?

In a real banking system the pillars never operate in isolation. Encapsulation protects data inside each class. Abstraction hides the complexity behind a clean service interface. Inheritance lets new account types reuse existing code. Polymorphism lets existing code handle new account types without modification.

JAVA
// Encapsulation: Account.balance is private, changed only through deposit/withdraw
// Abstraction: PaymentService.transfer() hides NEFT/UPI/RTGS complexity
// Inheritance: SavingsAccount extends Account, reuses deposit/withdraw
// Polymorphism: account.withdraw(amount) behaves differently for each account type

// The real power: adding a NEW account type requires ZERO changes to existing code.
public class NRIAccount extends Account {
    private String foreignCurrency;
    private BigDecimal exchangeRate;

    @Override
    public void withdraw(BigDecimal amountInINR) {
        // Custom validation: check FEMA compliance, check repatriation limits
        super.withdraw(amountInINR);
    }
}
// InterestBatchJob, PaymentService, TransferController: NONE of them need changes.

How is encapsulation different from abstraction?

Encapsulation and abstraction are different but complementary. Encapsulation is about data protection: making fields private and controlling how they change. Abstraction is about complexity hiding: exposing a simple interface and concealing how it works internally.

| Aspect | Encapsulation | Abstraction |
|--------|--------------|-------------|
| Focus | Protect internal state | Simplify external interface |
| Question answered | Who can access my data? | What does the user need to know? |
| Direction | Inward-facing (protect my internals) | Outward-facing (simplify for callers) |
| Java mechanism | private fields, validated setters | interface, abstract class |
| Banking example | balance is private, only withdraw() can change it | transfer() hides 15 internal steps |

What are common mistakes with OOP pillars?

The most common mistake is using inheritance for code reuse when composition is more appropriate. Another mistake is exposing a live internal collection from a getter, which breaks encapsulation.

JAVA
// WRONG: using inheritance just for code reuse
public class AccountReport extends Account {
    // AccountReport IS-NOT-A Account. It just needs account data.
}

// CORRECT: use composition
public class AccountReport {
    private final Account account; // HAS-A Account
    public AccountReport(Account account) { this.account = account; }
}

// WRONG: leaking internal collection
public List<Transaction> getTransactions() {
    return transactions; // caller can call .clear()!
}

// CORRECT: return unmodifiable view
public List<Transaction> getTransactions() {
    return List.copyOf(transactions);
}

When should you use each pillar?

Encapsulation should be applied to every class: make fields private and provide validated access methods. Abstraction should be applied whenever a caller does not need to know the implementation details: use interfaces for service contracts. Inheritance should be applied only when there is a true IS-A relationship with shared state: SavingsAccount IS-A Account. Polymorphism enables the Open/Closed Principle: existing code works with new types without modification.

What should a senior engineer know?

  • The four pillars are not design patterns you apply. They are fundamental properties of every Java class. Even a single class with no explicit extends demonstrates all four pillars (private fields, public methods, overloaded methods, implicit extends Object).
  • The Open/Closed Principle depends on polymorphism: code is open for extension (new subclasses) and closed for modification (existing code unchanged).
  • In Spring Boot, loose coupling is achieved through interfaces (abstraction) + constructor injection (DI). Services depend on interfaces, not concrete classes.
  • Effective Java recommends "favor composition over inheritance" because inheritance creates tight coupling (fragile base class problem).
  • Records (Java 16) enforce encapsulation through immutability: all fields are private final with no setters.
  • Sealed classes (Java 17) give the parent control over which children can exist, enabling exhaustive pattern matching in switch.

Visual Overview

DIAGRAM
  ┌─────────────────────────────────────────────┐
  │            FOUR PILLARS OF OOP               │
  ├─────────────────────────────────────────────┤
  │                                              │
  │  ENCAPSULATION        ABSTRACTION            │
  │  ┌──────────────┐    ┌──────────────┐       │
  │  │ private data  │    │ public API   │       │
  │  │ + validated   │    │ hides HOW    │       │
  │  │   methods     │    │ exposes WHAT │       │
  │  └──────────────┘    └──────────────┘       │
  │                                              │
  │  INHERITANCE          POLYMORPHISM           │
  │  ┌──────────────┐    ┌──────────────┐       │
  │  │ parent → child│    │ same call    │       │
  │  │ code reuse   │    │ different    │       │
  │  │ IS-A         │    │ behavior     │       │
  │  └──────────────┘    └──────────────┘       │
  └─────────────────────────────────────────────┘

  Account (abstract)
     │
     ├── SavingsAccount
     │     calculateInterest() → balance x 4%
     │
     ├── CurrentAccount
     │     calculateInterest() → 0
     │     withdraw() → allows overdraft
     │
     └── NRIAccount
           withdraw() → FEMA compliance check

  InterestBatchJob iterates List<Account>
     │
     ▼
  account.calculateInterest()  ← polymorphic call
     │
     ├── SavingsAccount → balance x 4% / 12
     ├── CurrentAccount → 0
     └── NRIAccount     → balance x 3.5% / 12

Key Takeaway

The four pillars of OOP are encapsulation, abstraction, inheritance, and polymorphism, and they work together, not in isolation. Encapsulation bundles data with methods and restricts access: the Account class keeps balance private and changeable only through deposit() and withdraw(), which validate every modification. Abstraction hides implementation complexity: the caller calls transfer() without knowing the 15 internal steps. Inheritance enables code reuse: SavingsAccount, CurrentAccount, and NRIAccount all extend Account, inheriting deposit() and getBalance() without duplication. Polymorphism enables flexibility: account.calculateInterest() automatically applies the correct formula for each account type at runtime.

In Pankaj's banking platform at National Bank of India, the month-end interest batch iterates over List and calls account.calculateInterest() on each. The JVM dispatches to the correct override: SavingsAccount returns balance multiplied by 4% divided by 12, CurrentAccount returns zero, NRIAccount returns balance multiplied by 3.5% divided by 12. The batch code has zero knowledge of specific account types. When the bank later adds FDAccount, the batch job processes it automatically without any code change. This is polymorphism and abstraction enabling the Open/Closed Principle.

The real test of OOP understanding is extensibility: when you add a new type (NRIAccount), existing code (InterestBatchService, AccountController) should require zero modifications. This is achieved by encapsulating behavior in each class, abstracting common behavior in the parent, inheriting shared functionality, and using polymorphism so callers work with the parent type.

Remember one sentence: encapsulation protects, abstraction simplifies, inheritance reuses, and polymorphism flexes, and together they let you add new types without changing existing code.

---

Share
Use ← → arrow keys to navigate