Skip to content

OOP Pillars#

Concept illustration
flowchart LR
  E([Encapsulation<br/>state behind a barrier])
  A([Abstraction<br/>essential, not incidental])
  I([Inheritance<br/>is-a hierarchy])
  P([Polymorphism<br/>one name, many forms])
  E --> A --> I --> P

  classDef p fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
  class E,A,I,P p;

    classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
    classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
    classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
    classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
    classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
    classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
    classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
    classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
    classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
    classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
    class E,A,I,P service;

Four foundational ideas every OO language gives you: Encapsulation, Abstraction, Inheritance, Polymorphism - usually shortened to "EAIP" or just "the four pillars".

1. Encapsulation - state behind a barrier#

Bundle data with the methods that act on it, hide the internals, expose only a contract.

classDiagram
  class BankAccount {
    -balance: BigDecimal
    -ownerId: UUID
    -lock: Lock
    +deposit(amount)
    +withdraw(amount)
    +getBalance() BigDecimal
  }
  note for BankAccount "balance is private.\nNo external code can\nset it directly."
  • Fields stay private. Mutations go through methods that enforce invariants (no negative balance, no overdraft).
  • Without encapsulation, every callsite duplicates business rules - and one will forget.
  • The "barrier" can be a class, a module, a package, or even a process boundary.

2. Abstraction - essential, not incidental#

Expose what a thing does; hide how it does it. Abstractions are the API + the rules; implementations are the code.

classDiagram
  class PaymentGateway {
    <<interface>>
    +charge(amount, card) ChargeResult
    +refund(chargeId) RefundResult
  }
  class StripeGateway
  class AdyenGateway
  class FakeGateway
  PaymentGateway <|.. StripeGateway
  PaymentGateway <|.. AdyenGateway
  PaymentGateway <|.. FakeGateway
  • Callers depend on PaymentGateway, not on Stripe specifics.
  • Swap real ↔ fake in tests; real ↔ different vendor in prod.
  • Abstraction != generalisation. Adding levels "just in case" creates indirection without insight.

3. Inheritance - is-a hierarchy#

Reuse + specialise a base type's behaviour and contract.

classDiagram
  class Vehicle {
    +id
    +seats
    +start()
    +stop()
  }
  class Car {
    +trunkSize
    +openTrunk()
  }
  class Motorcycle {
    +hasSidecar: bool
  }
  class ElectricCar {
    -batteryKwh
    +chargeFor(minutes)
  }
  Vehicle <|-- Car
  Vehicle <|-- Motorcycle
  Car <|-- ElectricCar
  • Subtypes inherit fields + methods, can add their own, can override existing.
  • Misused for code reuse alone - prefer composition instead (see Composition over inheritance).
  • Multiple inheritance: avoided in Java/C#; available via mixins/traits/interfaces in Scala, Rust, Python.

4. Polymorphism - one name, many forms#

Same operation, different behaviour depending on the runtime type.

classDiagram
  class Shape {
    <<abstract>>
    +area() double
  }
  class Circle {
    +r: double
    +area() double
  }
  class Rectangle {
    +w: double
    +h: double
    +area() double
  }
  class Triangle {
    +base: double
    +height: double
    +area() double
  }
  Shape <|-- Circle
  Shape <|-- Rectangle
  Shape <|-- Triangle
double totalArea(List<Shape> shapes) {
  return shapes.stream().mapToDouble(Shape::area).sum();
}

Shape::area dispatches to Circle.area() / Rectangle.area() / etc. The caller doesn't care which.

Flavours#

Flavour Mechanism Example
Subtype / dynamic dispatch virtual methods, vtable Shape::area above
Ad-hoc method overloading print(int) vs print(String)
Parametric generics / templates List<T>, Map<K,V>
Coercion implicit conversion int → long in arithmetic
Duck typing runtime structural Python - "if it quacks like a duck…"

How the pillars compose#

flowchart LR
  subgraph Encap[Encapsulation barriers]
    M[Module / Class]
  end
  subgraph Abs[Abstraction]
    I[Interface]
  end
  subgraph Inh[Inheritance hierarchy]
    A[Abstract] --> B[Concrete]
  end
  subgraph Poly[Polymorphism]
    Call[Caller] -->|same call| Many[Many impls]
  end
  M --> I --> A
  B --> Many

  classDef p fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
  classDef s fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
  class M,I,A,B,Call,Many s;

    classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
    classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
    classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
    classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
    classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
    classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
    classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
    classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
    classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
    classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
    class M,I,A,B,Call,Many service;
  • Encapsulation gives you a boundary.
  • Abstraction defines what crosses that boundary.
  • Inheritance shares a contract among types.
  • Polymorphism lets callers work against the contract without knowing the concrete type.

Together: callers depend on abstractions; implementations are encapsulated; new behaviours plug in via inheritance + polymorphism.

Where these show up in this site#

  • Notification system - Channel is an abstraction with concrete PushChannel / SMSChannel / EmailChannel siblings (Abstraction + Polymorphism).
  • Logger framework - Appender interface, many implementations; loggers compose appenders (Abstraction + Composition).
  • Parking Lot / Vending Machine / Elevator - Vehicle, Item, Request hierarchies use Inheritance + Polymorphism.
  • Payment gateway - PaymentMethod interface with Card / UPI / Wallet / BNPL implementations (Abstraction).

Glossary & fundamentals#

Concepts referenced in this design. Each row links to its canonical page; the tag column shows whether it is a high-level (HLD) or low-level (LLD) concept.

Tag Concept What it is Page
HLD Service mesh sidecar mesh, mTLS, traffic policy service-mesh
LLD Async models futures / async-await / coroutines / actors async-models
LLD OOP pillars encapsulation, abstraction, inheritance, polymorphism oop-pillars
LLD Composition over inheritance prefer composition; small swappable parts composition-over-inheritance

Quick reference#

Why these four (and not more)#

The "four pillars" framing is a teaching device, not a formal classification. Different communities slice OO into different sets - Alan Kay's original OO emphasises message-passing more than inheritance; modern functional-OO blends like Scala/Kotlin emphasise immutability + traits.

The four pillars survive because they're the operations a designer reaches for first when shaping a system in an OO language.

Practical rules of thumb#

  • Encapsulation: if a field is public, you've decided it's part of your forever-API. Default to private.
  • Abstraction: every dependency in your domain layer is an interface. Concretions live at the composition root.
  • Inheritance: limit hierarchies to 2 levels. Any deeper, refactor to composition.
  • Polymorphism: prefer it over if (type == ...) chains. The compiler tells you when you missed a case (with sealed types / exhaustive matches).

Common myths#

  • "OOP means inheritance." Inheritance is the least important pillar in modern code. Encapsulation and abstraction matter more.
  • "Encapsulation = getters and setters." No - getters and setters are usually a sign of leaked encapsulation. Methods should do things, not just expose state.
  • "Polymorphism is slow." On the JVM and modern C++, virtual calls are typically a single indirection; megamorphic call-site cost matters only in extreme hot loops.

Where OO falls short#

  • Highly concurrent designs lean on immutability and message passing more than on encapsulating mutable state.
  • Data-pipeline code reads cleaner as functions over immutable data than as objects.
  • Many "OO patterns" disappear in functional languages (Strategy = a function, Visitor = pattern matching).

Use the pillars as a default; abandon any of them when a more local style serves the code better.

Refs#

  • Object-Oriented Software Construction - Bertrand Meyer (1988).
  • Clean Architecture - Robert C. Martin.
  • Domain-Driven Design - Eric Evans (encapsulation by bounded context).
  • Effective Java - items on classes & interfaces (15-25).
  • Alan Kay's emails about what he meant by "object-oriented".

FAQ#

What are the four pillars of object-oriented programming?#

Encapsulation hides state behind methods, Abstraction exposes essentials, Inheritance reuses a parent's behaviour, and Polymorphism lets one name mean many forms.

What is the difference between encapsulation and abstraction?#

Encapsulation packages data with the code that operates on it. Abstraction picks which details to expose. Encapsulation enforces; abstraction designs.

Is polymorphism only about method overriding?#

No. There is also ad-hoc polymorphism (overloading) and parametric polymorphism (generics). Subtype polymorphism via overriding is the most familiar in OOP.

Should I use inheritance or composition?#

Prefer composition unless there is a clean is-a relationship and you need to substitute one type for the base. Inheritance creates tight coupling that is hard to undo.

Are OOP pillars still relevant in modern languages?#

Yes. Even functional-leaning languages reuse the ideas: encapsulation as modules, polymorphism as type classes, and so on. The vocabulary still shapes interviews.

Further reading#

Curated, high-credibility sources for going deeper on this topic.