CMD Guide
HomeOO & Low-Level DesignOOD Foundations

ObjectOriented Basics

Object-oriented programming binds state and the code that operates on it into one unit (an object), and for overridable methods it routes the call through a hidden table of function pointers on the object, so the same call site can run different code depending on the object's actual runtime type. Two of the four classic pillars — inheritance-for-reuse and polymorphism — are consequences of that one mechanism: indirect dispatch through a per-class method table. The other two are complementary and do not require dispatch at all: encapsulation is access control (you can fully encapsulate a class that has no subtypes and no polymorphism), and abstraction is depending on a contract rather than a concrete type. And not every call dispatches through the table — static, private, and final calls are bound statically at compile time (in the example below, charge() is final, so it is not virtual; only the doCharge() hook is).

A class is the blueprint: it declares the fields each object holds and the methods callers may invoke. An object (instance) is one filled-in copy of that blueprint living at a real memory address. Everything below is built on one running example — a payments system that charges money through several processors — because the pillars only become concrete when you watch them move real values.

The worked example: a payment system

We need to charge a customer's order total through one of several payment providers. The caller (a checkout service) must not know or care which provider runs — it just calls charge(amountCents). Here is the whole shape in Java; the same design in Go follows.

// Abstraction: the contract the checkout depends on.
interface PaymentProcessor {
    Receipt charge(long amountCents);
}

// A base class that captures shared state + shared behavior.
abstract class BaseProcessor implements PaymentProcessor {
    private long totalCharged = 0;          // encapsulated: private field

    public final Receipt charge(long amountCents) {
        if (amountCents <= 0)               // shared guard, written once
            throw new IllegalArgumentException("amount must be > 0");
        Receipt r = doCharge(amountCents);  // polymorphic hook
        totalCharged += amountCents;        // mutate private state safely
        return r;
    }

    public long getTotalCharged() { return totalCharged; }  // controlled read

    protected abstract Receipt doCharge(long amountCents);  // subclasses fill in
}

// Inheritance: reuse BaseProcessor's guard + accounting, override only doCharge.
class StripeProcessor extends BaseProcessor {
    protected Receipt doCharge(long c) { return new Receipt("stripe", c, "st_" + c); }
}
class PaypalProcessor extends BaseProcessor {
    protected Receipt doCharge(long c) { return new Receipt("paypal", c, "pp_" + c); }
}

record Receipt(String provider, long amountCents, String ref) {}

The checkout code never mentions Stripe or PayPal:

PaymentProcessor p = pickProcessor(order);  // returns Stripe OR Paypal
Receipt r = p.charge(1999);                  // $19.99 — one call site, two possible bodies

The same design in Go, which has no inheritance — it composes a shared struct and satisfies an interface implicitly:

type PaymentProcessor interface {
    Charge(amountCents int64) (Receipt, error)
}

type Receipt struct{ Provider string; AmountCents int64; Ref string }

// Shared state + shared guard, embedded into each processor.
type base struct{ totalCharged int64 }

func (b *base) run(amountCents int64, do func(int64) Receipt) (Receipt, error) {
    if amountCents <= 0 {
        return Receipt{}, fmt.Errorf("amount must be > 0")
    }
    r := do(amountCents)
    b.totalCharged += amountCents
    return r, nil
}

type Stripe struct{ base }   // embedding = reuse, not inheritance
func (s *Stripe) Charge(c int64) (Receipt, error) {
    return s.run(c, func(c int64) Receipt { return Receipt{"stripe", c, "st"} })
}

Java reuses by subclassing; Go reuses by embedding a struct and depends on the interface by structural match. Both reach the same goal — one stable contract, swappable bodies — which is the real point of the pillars.

Tracing the four pillars through one call

Follow p.charge(1999) where p actually points to a StripeProcessor. Each step is one pillar doing concrete work.

StepWhat happens with the value 1999Pillar at work
1Caller holds p typed as PaymentProcessor; it cannot read totalCharged directly.Encapsulation
2Caller invokes charge(1999) knowing only the interface, not the class.Abstraction
3charge runs in BaseProcessor — the guard 1999 > 0 passes — code the subclass never rewrote.Inheritance (reuse)
4The call to doCharge(1999) dispatches to StripeProcessor.doCharge via the object's method table, returning Receipt("stripe", 1999, "st_1999").Polymorphism
5Back in the base, totalCharged goes 0 → 1999; only getTotalCharged() can read it.Encapsulation

If p had pointed to a PaypalProcessor, steps 1–3 and 5 are byte-for-byte identical; only step 4's table entry differs, yielding "pp_1999". That is polymorphism's payoff: the variation is isolated to one overridden method, and the caller is untouched.

diagram
diagram

Pitfalls

Selection & trade-offs: inheritance vs composition

The decision you will actually make over and over is how to reuse and vary behavior — the heart of every OOD problem. The two tools are inheritance (subclass overrides parent methods) and composition (an object holds another object and delegates to it, e.g. via the Strategy pattern). They look interchangeable; they are not.

Decision criteria — reach for inheritance when: the relationship is a genuine, permanent is-a (a StripeProcessor is a PaymentProcessor); the subtype is fully substitutable for the parent; and the variation is fixed at compile time. Reach for composition when: the relationship is has-a / uses-a; behavior must change at runtime; or you'd otherwise need multiple inheritance dimensions (provider × currency × retry policy) that subclassing would explode into a combinatorial class tree.

What inheritance costs: tight coupling to the base — a change in BaseProcessor can break every subclass (fragile base class); only one parent in Java; and behavior frozen at construction. What composition costs: more classes and one extra layer of indirection (the delegate field plus its wiring), and slightly more ceremony to set up. What composition buys: runtime swappability, independent testing of each strategy, and no inheritance-tree explosion.

Trace the choice for our example: if the retry policy must change per-merchant at runtime, subclassing fails — you'd need StripeWithRetry, StripeNoRetry, PaypalWithRetry… Instead inject a RetryStrategy object into the processor (composition). Conversely, the shared charge guard is permanent and identical for all processors, so capturing it in a base class (inheritance) is the right call — which is exactly what the example does.

Choose inheritance when the subtype is a permanent, substitutable is-a and the variation is fixed; prefer composition when behavior must vary at runtime or along more than one axis. The industry default — “favor composition over inheritance” — exists because the runtime-flexibility need shows up far more often than people expect.

Interface, abstract class, or concrete class?

The payment example already uses all three without naming them: PaymentProcessor is an interface, BaseProcessor is an abstract class, and StripeProcessor is concrete. The staff-level question is not "what are they?" but "which one do I reach for first?"

ShapeCarries state?Carries implementation?How many can a class extend?Use when
Interfaceno (constants only)only default methods, no instance fieldsmanyUnrelated types need to promise the same capability: what can I do?
Abstract classyesyes — concrete methods + abstract hooksone (Java)Subtypes are genuinely the same kind of thing and share real state/behavior: what am I?
Concrete classyesyesoneThe thing itself, fully instantiable; subclass only when the subtype is truly substitutable.

The tie-break question: do these types share mutable state and identity, or only a promise? State and identity ⇒ abstract class (or Go embedding). Promise only ⇒ interface. If you find yourself creating an abstract class just to reuse a method while the subtypes have no shared state, you actually wanted an interface plus a small helper class.

When inheritance breaks: a concrete refactor story

Suppose BaseProcessor starts life with the guard before doCharge, as shown above. A year later someone adds fraud scoring and refactors the base to run doCharge first, then validate the result:

// BaseProcessor v2 — a well-intentioned refactor that breaks subclasses
public final Receipt charge(long amountCents) {
    Receipt r = doCharge(amountCents);            // subclass runs FIRST now
    if (amountCents <= 0 || r.amountCents() != amountCents)
        throw new IllegalArgumentException("invalid charge");
    totalCharged += amountCents;
    return r;
}

A subclass that relied on the guard running before its code now sees negative amounts and must defensively re-validate. Worse, a subclass written under the old contract may have assumed doCharge was only ever called with positive amounts and used that assumption to skip its own checks. The base changed the precondition of the hook; every subclass broke.

This is the fragile-base-class problem: a change in the parent silently invalidates assumptions buried in subclasses the author has never read. Composition avoids it because the shared behavior lives in a separate object with an explicit, stable contract. Here is the same guard expressed compositionally:

// Shared behavior as a wrapper, not a parent class
class ValidatingProcessor implements PaymentProcessor {
    private final PaymentProcessor delegate;
    ValidatingProcessor(PaymentProcessor delegate) { this.delegate = delegate; }

    public Receipt charge(long amountCents) {
        if (amountCents <= 0)
            throw new IllegalArgumentException("amount must be > 0");
        return delegate.charge(amountCents);   // Stripe/PayPal know nothing about validation
    }
}

Now a future change to validation touches only ValidatingProcessor; StripeProcessor and PaypalProcessor remain untouched. That is the real meaning of "favor composition over inheritance": not "never use inheritance," but "don't let shared behavior become a hidden contract with every subclass."

Takeaways


Re-authored and deepened for this guide. Grounded in Educative's Grokking the Low Level Design Interview Using OOD Principles (object/class/four-pillars framing), Joshua Bloch's Effective Java (Item 18 “Favor composition over inheritance”, Item 10 on equals/hashCode, Items 19–20 on interfaces and abstract classes), Gamma et al. Design Patterns (Strategy and the composition-over-inheritance principle), and Barbara Liskov's substitution principle. The original page gave one-sentence definitions with no mechanism, code, or example for inheritance; this version adds a worked Java + Go example, a traced dispatch, a method-table diagram, real pitfalls, an interface/abstract-class/concrete-class decision table, and a fragile-base-class refactor story.

Practice drills

Work these against the payment example on this page; each targets one mechanism it already traced.

  1. Trace p.charge(1999) and name the pillar doing the work at each of the five steps, then say which single step changes if p is a PaypalProcessor instead.
  2. Explain why getTotalCharged() returning a mutable collection (if it held one) would break the guard in charge — and what to return instead.
  3. The retry policy must now vary per merchant at runtime. Show why subclassing BaseProcessor explodes here, and refactor to inject a RetryStrategy (composition).
  4. Reproduce the fragile-base-class break: change BaseProcessor.charge to call doCharge before the guard, and describe the precondition every subclass silently loses.
🤖 Don't fully get this? Learn it with Claude

Stuck on ObjectOriented Basics? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🎨 Explain it visually

Build the mental picture, not memorization.

I just read a lesson on **ObjectOriented Basics** (OO & Low-Level Design) and want to truly understand it. Explain ObjectOriented Basics from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🤔 Walk me through it (interactive)

Socratic — adapts to where you're stuck.

Teach me **ObjectOriented Basics** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧪 Quiz me & fix my gaps

Active recall exposes what you missed.

Quiz me on **ObjectOriented Basics** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧠 Make it stick

Intuition + hook + flashcards for long-term memory.

Help me remember **ObjectOriented Basics** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.

📝 My notes