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 bodiesThe 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.
| Step | What happens with the value 1999 | Pillar at work |
|---|---|---|
| 1 | Caller holds p typed as PaymentProcessor; it cannot read totalCharged directly. | Encapsulation |
| 2 | Caller invokes charge(1999) knowing only the interface, not the class. | Abstraction |
| 3 | charge runs in BaseProcessor — the guard 1999 > 0 passes — code the subclass never rewrote. | Inheritance (reuse) |
| 4 | The call to doCharge(1999) dispatches to StripeProcessor.doCharge via the object's method table, returning Receipt("stripe", 1999, "st_1999"). | Polymorphism |
| 5 | Back 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.
Pitfalls
- Leaky encapsulation via getters that return mutable internals. A getter returning the live
Listor array field lets callers mutate private state behind your back, defeating the guard incharge. Return a copy or an unmodifiable view. - Confusing “has a getter for every field” with encapsulation. Auto-generating
get/setfor all fields is just public state with extra steps. Real encapsulation exposes operations (charge), not raw fields. - Inheriting for code reuse alone. Subclassing to grab a method you like (e.g. extending
ArrayListto reuseadd) couples you to the parent's entire contract and breaks the moment the parent changes — the classic fragile-base-class problem. Reuse by composition instead unless the subtype is truly substitutable. - Overriding that breaks substitutability (LSP violation). If
PaypalProcessor.doChargesilently returnsnullon amounts over a cap whileStripeProcessorthrows, polymorphism becomes a trap: callers can't reason about the interface anymore. All implementations must honor the same contract. - Object identity vs equality. Two
Receiptobjects with the same fields are not==in Java (that compares references). Overrideequals/hashCode(therecorddoes this for you) or comparisons and hash-map lookups silently fail — e.g. aHashSet<User>keyed onidwill store duplicates and miss lookups if you overrideequalsbut forgethashCode.
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?"
| Shape | Carries state? | Carries implementation? | How many can a class extend? | Use when |
|---|---|---|---|---|
| Interface | no (constants only) | only default methods, no instance fields | many | Unrelated types need to promise the same capability: what can I do? |
| Abstract class | yes | yes — concrete methods + abstract hooks | one (Java) | Subtypes are genuinely the same kind of thing and share real state/behavior: what am I? |
| Concrete class | yes | yes | one | The 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
- Only two pillars derive from the method-table indirection between caller and concrete implementation: inheritance reuses a base body, and polymorphism picks the body at runtime (for overridable calls only —
static/private/finalcalls, likecharge()here, bind statically). The other two are independent: encapsulation hides state behind operations (access control), and abstraction is the contract the caller depends on — both hold with zero polymorphism. - Polymorphism's value is that variation collapses into a single overridden method — the call site and all shared code stay identical, as the Stripe/PayPal trace shows (only step 4 differs).
- Encapsulation means exposing operations, not generating a getter/setter for every field; a getter that leaks a mutable internal is no encapsulation at all.
- Interface = capability promise across unrelated types; abstract class = shared state/behavior among genuine subtypes; concrete class = the instantiable thing itself.
- Inheritance is fragile because a base-class change can silently violate subclass assumptions; composition isolates shared behavior behind an explicit wrapper/delegate so changes stay local.
- The recurring real decision is inheritance vs composition: inheritance for permanent substitutable is-a with fixed variation, composition for runtime-swappable or multi-axis behavior. Default to composition.
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.
- Trace
p.charge(1999)and name the pillar doing the work at each of the five steps, then say which single step changes ifpis aPaypalProcessorinstead. - Explain why
getTotalCharged()returning a mutable collection (if it held one) would break the guard incharge— and what to return instead. - The retry policy must now vary per merchant at runtime. Show why subclassing
BaseProcessorexplodes here, and refactor to inject aRetryStrategy(composition). - Reproduce the fragile-base-class break: change
BaseProcessor.chargeto calldoChargebefore 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.
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.
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.
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.
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.