Engineering Atlas › Low-Level Design

Object-oriented programming at a glance

14 min read · last reviewed 2026-10-17

Open in the interactive app: practice, quiz, mock interview →

In one minute

How it works

The fifteen at a glance

Each row links to the lesson in this course that goes deeper.

#ConceptThe idea in one lineLesson
1ClassA type that bundles state with the operations allowed on itClasses and Objects
2ObjectOne instance with identity, its own state, and the class's behaviourClasses and Objects
3Constructor and invariantsCreation either produces a valid object or fails; no half-built objectsClasses and Objects, Builder
4Enum and value objectA closed set of named values, and immutable values compared by contentEnums, Classes and Objects
5EncapsulationState changes only through methods that keep the rulesEncapsulation
6AbstractionCallers depend on what an object does, not howAbstraction
7InterfaceA named set of operations with no implementationInterfaces
8Abstract classA partial implementation that subclasses completeAbstraction, Template Method
9InheritanceA subclass reuses and specialises a parent's code and typeInheritance, Liskov Substitution Principle
10PolymorphismOne call site, behaviour chosen by the receiver's runtime typePolymorphism
11Overriding vs overloadingDynamic dispatch on the receiver versus compile-time choice by signaturePolymorphism
12AssociationOne object knows and uses another long termAssociation
13AggregationA whole groups parts that live on without itAggregation
14CompositionA whole owns its parts; they are created and destroyed with itComposition
15Dependency and realizationShort-term use of another type, and a class implementing an interfaceDependency, 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.

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

  1. Programs fail when data and the rules about it drift apart, so objects keep them together and hide the data.
  2. Hidden data means callers depend only on operations, so implementations can vary behind one interface.
  3. Varying implementations need a runtime choice, so languages add a dispatch table per type, one pointer chase per call.
  4. Inheritance couples the subtype to the parent's internals, so designers prefer composing objects through interfaces and keep inheritance for true "is a" relationships.
  5. 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 : owns

Worked 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

OptionStrengthsWeaknessesChoose when
Interface plus several implementationsSubstitutable; testable with fakes; no shared internalsOne indirection per call; behaviour spread across filesReal variants exist today
Inheritance from a base classShares code and type in one stepFragile base class; subtypes coupled to parent internalsA true "is a" with shared invariants, or a framework hook
Composition of partsParts replaceable at runtime; clear ownershipMore wiring code; delegation boilerplateBehaviour built from pieces, or chosen by configuration
Plain data plus functionsSimple; easy to serialise; no dispatch costRules can be bypassed; invariants live in callersData transfer objects, hot loops, pipelines
Enum plus switchClosed set, compiler-checked exhaustivenessAdding a variant edits every switchFew variants, owned by one team, rarely extended

Pitfalls in production

Leadership lens

Check yourself

  1. 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.

  2. Why is "prefer composition over inheritance" good advice for a payment checkout?

    Show answer

    A checkout composed of PaymentMethod objects 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.

  3. 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.

  4. 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

Free resources

Open in the interactive app: practice, quiz, mock interview →