Menu

Earn Premium with Referrals

Invite your friends and earn Premium rewards through our referral program.

See how it works and start inviting friends.

Abstraction
LLD

Abstraction

Understand abstraction and how to expose essential behavior while hiding implementation details.

The Decision Beginners Get Wrong

Both tools define “a type you cannot instantiate,” so beginners treat them as interchangeable and pick arbitrarily — then discover the cost later. The wrong choice either locks a hierarchy (abstract class chosen where unrelated classes needed the capability) or duplicates code across implementations (interface chosen where shared skeleton existed). One question separates them: does the type need to carry state and shared implementation, or only a contract?

Capability Matrix

CapabilityInterfaceAbstract Class
Instance fields (state)❌ (static final only)
Constructors✅ (chained by subclasses)
Multiple inheritance of type❌ (single extends)
Method bodiesdefault/static/private methods (Java 8/9+)Full
Final methods
Member access modifiersimplicitly publicAny

The state row is decisive: interfaces describe capabilities mixed into any class; abstract classes describe partial implementations sharing real fields.

Decision Rule

  • CAN-DO capability → interface: Comparable, AutoCloseable, PaymentProcessor. Mixable into any hierarchy.
  • IS-A with shared code/state → abstract class: AbstractList supplies a list skeleton; subclasses share actual implementation.
      interface Processor          abstract class BaseProcessor
     ┌───────────────────┐        ┌─────────────────────────┐
     │ + process(Item)   │        │ - stats: Counter  (state)│
     └─────────▲─────────┘        │ + process() { template } │
       ┌───────┼────────┐         │ # hook(): void  (protected)│
       │       │        │         └──────────▲──────────────┘
 CardProc  UpiProc  WalletProc            │ extends (one only)
 (each free to extend anything else)   PdfProc, CsvProc

Left tree: implementers share nothing but the contract and may extend anything else. Right tree: subclasses inherit working machinery but surrender their single extends slot to do it.

Template Method — the abstract-class sweet spot

abstract class ReportGenerator {
    public final Report generate(Data d) {   // fixed algorithm
        var rows = transform(d);             // subclass varies steps,
        validate(rows);                      // invariant enforced in base
        return render(rows);
    }
    protected abstract List<Row> transform(Data d);
}

Interfaces cannot hold this skeleton with enforced ordering and validation — that alone often decides.

Edge Cases

  • Default-method diamond: two interfaces provide the same default → compiler forces an override; disambiguate via A.super.method().
  • Partial interface implementation: abstract class implements some methods, leaves rest abstract for its own subclasses.
  • Lambdas: implement only functional interfaces — no abstract-class shortcut exists.
  • Sealed classes (Java 17): permits restricts who extends/implements either — a modern middle ground controlling hierarchy spread.

Failure Modes

  • Abstract class used purely for code reuse without IS-A (Manager extends User) — locks hierarchy; use composition.
  • Interface grown to thirty methods (“just add it here”) — every mock in tests implements thirty stubs (ISP violation).
  • Choosing abstract class “for future flexibility” with zero shared code today — inheritance cost paid, benefit never collected.

The Problem It Solves

A developer calling repo.find(id) should not need to know about SQL parsing, connection pools, retries, or cache invalidation — yet without deliberate abstraction, all of that leaks into callers through signatures, exceptions, and timing behavior. Unmanaged complexity compounds: each caller that knows internal details becomes another place that breaks when internals change. Abstraction is complexity management — exposing what an operation does while hiding how, including that it may be expensive.

            caller's view                 hidden subsystem
        ┌───────────────┐          ┌──────────────────────────┐
        │ repo.find(id) │ ───────► │ SQL parse → plan → I/O   │
        │  (one concept)│          │ cache check, retries     │
        └───────────────┘          │ connection pool...       │
       "find" is stable            └──────────────────────────┘
       implementations churn              free to change

The boundary line is the contract. Left of it, one concept; right of it, arbitrary machinery. Everything to the right can be rewritten — swap Postgres for a cache-first service — without a single caller changing, because callers never knew.

Levels of Abstraction

LayerQuestion answeredMust not know
ControllerWhich use case?SQL, retry policy
DomainBusiness rules?HTTP status codes
RepositoryHow is this type stored?Caller identity
Driver/infraBytes, socketsBusiness meaning

Each row answers exactly its own question and is ignorant of everything above it. When a layer reaches outside its cell, the abstraction has leaked.

Leaky Abstractions (the core failure mode)

  • UserRepository.findAll() internally streams ten million rows — signature promises simplicity, runtime delivers OOM. Cost leaked through a simple-looking contract.
  • Network call behind a local-looking method (service.save(x) doing RPC) — latency and partial-failure semantics invisible to callers.
  • Rule: if an operation can be slow or fail in ways locals cannot, name it (fetchFromRemote, return a future) or wrap the semantics explicitly.

Law of Demeter (principle of least knowledge)

  • Talk only to: this, parameters, fields, objects you create.
  • Violation: order.getCustomer().getAddress().getCity() — caller couples to three internal structures; any of them changing breaks this line.
  • Fix — tell, don’t ask: order.getShippingCity() moves traversal behind the owner.

Interview Application

  • Name abstraction levels aloud: “controllers won’t know storage exists — repository interface here.”
  • Complexity-hiding wins follow-ups: adding caching under find() touches zero callers — say that sentence.
  • Anti-signal: exposing EntityManager/Connection through domain APIs — infrastructure types leaking upward.

Failure Modes

  • Wrong-level abstraction: single doProcess(ctx Map) god-method — hides so much behavior becomes undiscoverable.
  • Speculative layers: interface + impl + factory where one concrete path exists and no test seam is planned — indirection tax forever.
  • Transitive coupling via DTOs: returning JPA entities from controllers makes the DB schema public API.

My Private Notes

Notes are auto-saved locally to this device.