OOP Pillars — Detailed#
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
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 —
Channelis an abstraction with concretePushChannel / SMSChannel / EmailChannelsiblings (Abstraction + Polymorphism). - Logger framework —
Appenderinterface, many implementations; loggers compose appenders (Abstraction + Composition). - Parking Lot / Vending Machine / Elevator —
Vehicle,Item,Requesthierarchies use Inheritance + Polymorphism. - Payment gateway —
PaymentMethodinterface withCard / UPI / Wallet / BNPLimplementations (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 |