Object-oriented programming at a glance
Open in the interactive app: practice, quiz, mock interview →In one minute
- Object-oriented programming is a way to keep rules next to the data they protect: an object owns some state and exposes only operations that keep that state valid.
- Fifteen concepts cover almost every LLD discussion. They fall into four groups: building blocks (class, object, constructor, enum and value object), protection (encapsulation, abstraction), substitution (interface, abstract class, inheritance, polymorphism, overriding versus overloading), and relationships (association, aggregation, composition, dependency and realization).
- The concepts are not a checklist; they are connected. Encapsulation makes abstraction possible, interfaces make polymorphism cheap to use, and composition is usually a safer route to polymorphism than inheritance.
- Each concept has a runtime price: a vtable or itab pointer, an extra indirection, a heap allocation. Usually it is nanoseconds; in hot loops it is worth knowing.
- When reviewing agent-written code, these concepts give you the vocabulary to say what is wrong: "this setter breaks the invariant", "this subclass is not substitutable", "this should own its parts".
How it works
The fifteen at a glance
Each row links to the lesson in this course that goes deeper.
| # | Concept | The idea in one line | Lesson |
|---|---|---|---|
| 1 | Class | A type that bundles state with the operations allowed on it | Classes and Objects |
| 2 | Object | One instance with identity, its own state, and the class's behaviour | Classes and Objects |
| 3 | Constructor and invariants | Creation either produces a valid object or fails; no half-built objects | Classes and Objects, Builder |
| 4 | Enum and value object | A closed set of named values, and immutable values compared by content | Enums, Classes and Objects |
| 5 | Encapsulation | State changes only through methods that keep the rules | Encapsulation |
| 6 | Abstraction | Callers depend on what an object does, not how | Abstraction |
| 7 | Interface | A named set of operations with no implementation | Interfaces |
| 8 | Abstract class | A partial implementation that subclasses complete | Abstraction, Template Method |
| 9 | Inheritance | A subclass reuses and specialises a parent's code and type | Inheritance, Liskov Substitution Principle |
| 10 | Polymorphism | One call site, behaviour chosen by the receiver's runtime type | Polymorphism |
| 11 | Overriding vs overloading | Dynamic dispatch on the receiver versus compile-time choice by signature | Polymorphism |
| 12 | Association | One object knows and uses another long term | Association |
| 13 | Aggregation | A whole groups parts that live on without it | Aggregation |
| 14 | Composition | A whole owns its parts; they are created and destroyed with it | Composition |
| 15 | Dependency and realization | Short-term use of another type, and a class implementing an interface | Dependency, Realization |
Building blocks
A class is a type plus rules; an object is one instance, and the key question is whether it has identity. Two bank accounts with the same balance are still different accounts; two Money(500, "EUR") value objects are interchangeable, so they should be immutable and compared by content. Enums close a set (PENDING, PAID, REFUNDED) so a tool can flag a switch that misses a case. The constructor is where invariants start: if a Card needs a four-digit suffix, the constructor rejects anything else, and every method after that can assume it.
Protection
Encapsulation means outside code cannot put an object into an invalid state. A wallet exposes charge(amount), which checks the balance, not a public balance field that anyone can set to minus 400. Abstraction is the other face: callers see charge and available, and never learn whether the balance lives in a field, a ledger or a remote service. Encapsulation protects the object from callers; abstraction protects callers from the object's internals. Without encapsulation there is nothing stable to abstract over.
Substitution
An interface names a capability (PaymentMethod: label, available, charge, refund). Polymorphism lets one call site, method.charge(amount), run the card's, the wallet's or the gift card's logic depending on the object. Inheritance is one way to get polymorphism, by subclassing; it also shares code, which is where it gets into trouble, because a subclass that changes a promised behaviour breaks every caller (Liskov Substitution Principle). An abstract class sits between: shared code plus abstract methods, the backbone of Template Method. Overriding replaces a method in a subtype and is resolved at runtime; overloading picks among methods with the same name by argument types at compile time (Java, C++, C#). Python, Go and TypeScript have no real overloading, which is one reason idioms differ across these languages.
Relationships
Relationships say who knows whom and who owns whom. Association: an order knows its customer. Aggregation: a team groups employees who outlive the team. Composition: an order owns its line items; deleting the order deletes them, and nobody else holds them. Dependency: a checkout uses a clock passed into a method and keeps no reference. Realization: Card implements PaymentMethod. These map directly onto lifecycle code: who constructs, who deletes, who may hold a reference. Getting them wrong produces dangling references and leaked objects, the object-level version of the partial failure shape.
How the concepts connect
The table hides the most useful part, which is how the concepts lean on each other.
- Encapsulation is the precondition for everything else: an object whose fields anyone can change cannot promise anything to an interface.
- Interfaces plus realization give polymorphism without sharing code; inheritance gives polymorphism with shared code and tighter coupling.
- Composition plus an interface (a checkout holding a list of
PaymentMethods) gives the same flexibility as a subclass hierarchy, with each piece replaceable at runtime. That is the root of "prefer composition over inheritance", and of the Strategy, Decorator and Bridge patterns. - Dependency on interfaces rather than concrete classes is the Dependency Inversion Principle, and it is what makes objects testable with fakes.
- Value objects make encapsulation cheap: immutable values need no defensive copies and are safe to share across threads.
- The principles judge how well you used them: Coupling and Cohesion, the SOLID lessons, Law of Demeter.
Reviewing agent-written code
Agents' OOP failures are predictable: setters for every field (encapsulation in name only), isinstance switches where a polymorphic method belongs, deep inheritance to reuse two helpers, and "manager" classes holding all the logic while entities are bags of data (the anemic domain model). Ask, for each class: what invariant does it protect, which methods can break it, and could a new subtype be added without editing a switch?
The same shape elsewhere
Polymorphism through a table of function pointers is older than OOP and lives far outside it. The Linux kernel's virtual file system gives every filesystem a struct file_operations with read, write and open pointers, so read(2) works the same on ext4, procfs and a socket. Device drivers, TLS libraries with pluggable cipher suites and database drivers all use the same structure: an interface, several realizations, and callers that never know which one they have.
Down the stack
Layer 1The contract
Each concept promises something testable. Encapsulation: no sequence of public calls can violate the class invariant (a wallet's balance never goes negative). Polymorphism: a call through the interface does what the interface documents for every implementation, so one contract test suite passes against all of them. Composition: when the whole is destroyed or reset, its parts go with it, and no outside reference to a part exists. Value objects: equal values are interchangeable and never change after construction. A design review can ask for a test per promise.
Layer 2The mechanism
Polymorphism is implemented by dispatch tables. In C++ each object of a class with virtual methods carries a hidden pointer (vptr) to a per-class vtable, an array of function pointers laid out by the Itanium C++ ABI; p->charge() loads the vptr, loads the slot, and calls it. Go has no classes: an interface value is two words, a pointer to an itab (the interface type, the concrete type and its method pointers, built lazily and cached in a global hash table) and a pointer to the data. JavaScript looks up methods along the prototype chain, made fast by hidden classes (V8 calls them maps) and inline caches that remember the last shape seen at a call site. Python looks up attributes in the instance __dict__, then along the class's MRO, with a per-type method cache; __slots__ replaces the per-instance dict with fixed offsets.
Layer 3The machine
The costs are small but real. A direct call is about 1 ns and inlinable; a virtual or interface call is 1 to 3 ns when the branch predictor guesses right, and up to around 15 to 20 ns when a call site sees many types and mispredicts, plus the lost inlining, which often matters more. Every C++ polymorphic object pays 8 bytes for its vptr; every Go interface value is 16 bytes, and storing a non-pointer value in one usually forces a heap allocation. A Java object header is 12 to 16 bytes. A small Python object with a __dict__ costs over 100 bytes, against roughly 50 to 60 with __slots__. Composition affects layout: a Go struct that embeds its parts by value keeps them in one contiguous block (one cache line for small parts), whereas a list of pointers to parts scatters them across the heap and costs a cache miss, about 100 ns, per part. Encapsulation is also where concurrency control lives: if only the wallet's methods touch the balance, a single mutex inside the wallet makes it thread safe; if the field is public, no lock can.
Why it's built this way
- Programs fail when data and the rules about it drift apart, so objects keep them together and hide the data.
- Hidden data means callers depend only on operations, so implementations can vary behind one interface.
- Varying implementations need a runtime choice, so languages add a dispatch table per type, one pointer chase per call.
- Inheritance couples the subtype to the parent's internals, so designers prefer composing objects through interfaces and keep inheritance for true "is a" relationships.
- Dispatch and indirection cost cache misses in hot loops, so performance-critical code devirtualises, inlines, or stores parts by value.
Diagram
classDiagram
class PaymentMethod {
<<interface>>
+label() string
+available(today) int
+charge(amount, today)
+refund(amount)
}
class Card
class Wallet
class GiftCard
class Checkout {
+pay(amount, today)
+refund(receiptId)
}
class Receipt
PaymentMethod <|.. Card
PaymentMethod <|.. Wallet
PaymentMethod <|.. GiftCard
Checkout o-- PaymentMethod : uses in order
Checkout *-- Receipt : ownsWorked example
A food delivery app splits payment across a gift card, a wallet and a card, in that order. The first version was one 180-line PaymentService.pay(user, amount) with three if branches writing user.gift_balance, user.wallet_balance and user.card_used directly. Three bugs followed: a promotion script set wallet_balance directly and pushed 1,900 accounts negative; expired gift cards were spent because the expiry check lived in the checkout screen and the reorder flow skipped it; and a payment that failed on the card leg had already taken money from the wallet.
The redesign applied four concepts. Encapsulation: each method object owns its balance and rejects overdrafts, so no script can push it negative. Polymorphism: GiftCard.available(today) returns 0 after expiry, so every flow gets the rule. Composition: Checkout owns an ordered list of methods and its receipts. All-or-nothing: pay sums available across methods before charging anything. Adding loyalty points later was one new class and no edits to Checkout. The cost: a few nanoseconds of interface dispatch per leg, against a 150 ms card network call.
Trade-offs
| Option | Strengths | Weaknesses | Choose when |
|---|---|---|---|
| Interface plus several implementations | Substitutable; testable with fakes; no shared internals | One indirection per call; behaviour spread across files | Real variants exist today |
| Inheritance from a base class | Shares code and type in one step | Fragile base class; subtypes coupled to parent internals | A true "is a" with shared invariants, or a framework hook |
| Composition of parts | Parts replaceable at runtime; clear ownership | More wiring code; delegation boilerplate | Behaviour built from pieces, or chosen by configuration |
| Plain data plus functions | Simple; easy to serialise; no dispatch cost | Rules can be bypassed; invariants live in callers | Data transfer objects, hot loops, pipelines |
Enum plus switch | Closed set, compiler-checked exhaustiveness | Adding a variant edits every switch | Few variants, owned by one team, rarely extended |
Pitfalls in production
- Getters and setters for every field, which is encapsulation in name only. Fix: expose operations that express intent (
charge,refund) and drop setters that can break invariants. - Type switches (
isinstance,instanceof, Go type switches) scattered through business logic. Fix: move the varying behaviour into a method on the interface. - Deep inheritance trees built for code reuse, where a change in a base class breaks distant subclasses. Fix: extract the shared code into a component and compose it.
- Mutable value objects shared between owners, so changing one order's
Moneychanges another's. Fix: make values immutable and return new instances. - Composed parts leaked by returning internal lists, so callers mutate them behind the owner's back. Fix: return copies or read-only views, and test that mutating the returned list has no effect.
Leadership lens
- In design review, ask each class for its invariant in one sentence. A class that has none is either a value, a data carrier, or misplaced logic.
- Agree as a team on when inheritance is allowed (framework hooks, true subtype relationships) and default to composition otherwise; it removes a recurring review argument.
- Treat public mutable fields in domain classes as a review blocker. Production incidents from scripts writing state directly are common and avoidable.
- When onboarding engineers or prompting agents, link the concept table above to the codebase's own examples, so "use composition here" points at a real class.
Check yourself
What is the difference between encapsulation and abstraction?
Show answer
Encapsulation stops outside code from putting an object into an invalid state by hiding its data behind checked operations. Abstraction lets callers rely on what an object does without knowing how; it depends on encapsulation, because hidden data is what can be changed freely.
Why is "prefer composition over inheritance" good advice for a payment checkout?
Show answer
A checkout composed of
PaymentMethodobjects can change methods, order and count at runtime and test each in isolation; a subclass per combination would multiply classes and couple each to a base class's internals.Overriding and overloading both reuse a method name. How do they differ at runtime?
Show answer
Overriding is resolved at runtime through the receiver's dispatch table; overloading is resolved at compile time from the argument types, so it costs nothing at runtime and cannot vary by the object's dynamic type.
Why does storing a small struct in a Go interface often cost an allocation, and when does it matter?
Show answer
The interface's data word must point to the value, so a non-pointer value usually escapes to the heap. It matters in hot loops that create millions of interface values, where the GC pressure shows up in profiles.
Watch
- Object-Oriented Programming, Simplified (Programming with Mosh): a fast refresher on encapsulation, abstraction, inheritance and polymorphism with small examples.
- OOP Principles: Composition vs Inheritance (Dave Crabbe): the two ways to build behaviour from other types, compared in Python.
- The Four Principles of Object Oriented Programming (HuwsTube): a sceptical look at the usual four pillars and the jargon around them.
Free resources
- Go Data Structures: Interfaces (Russ Cox): how Go builds itabs and two-word interface values, and what dispatch costs.
- Fast properties in V8: hidden classes and inline caches, the machinery behind JavaScript method lookup.
- Data model (Python documentation): attribute lookup,
__slots__and special methods, where Python's object model is defined. - Itanium C++ ABI: the precise vtable layout that GCC and Clang use for virtual dispatch.
- On Understanding Data Abstraction, Revisited (William Cook): a clear essay on what objects are and how they differ from abstract data types.
- Anemic Domain Model (Martin Fowler): the failure mode where objects hold data and logic lives elsewhere.