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
| Capability | Interface | Abstract Class |
|---|---|---|
| Instance fields (state) | ❌ (static final only) | ✅ |
| Constructors | ❌ | ✅ (chained by subclasses) |
| Multiple inheritance of type | ✅ | ❌ (single extends) |
| Method bodies | default/static/private methods (Java 8/9+) | Full |
| Final methods | ❌ | ✅ |
| Member access modifiers | implicitly public | Any |
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:
AbstractListsupplies 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):
permitsrestricts 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
| Layer | Question answered | Must not know |
|---|---|---|
| Controller | Which use case? | SQL, retry policy |
| Domain | Business rules? | HTTP status codes |
| Repository | How is this type stored? | Caller identity |
| Driver/infra | Bytes, sockets | Business 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/Connectionthrough 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.
Premium Content
Unlock Abstraction and all premium lessons with a subscription.
From ₹199.99/year — See plans