Day 03 · Week 1 · Foundations

OOP, re-examined.

You know what classes and inheritance are. Today is the question your tutorial never answered: when do I actually pick an abstract class vs. an interface? What is a marker interface and why does Serializable exist? Why does everyone preach 'composition over inheritance'?

~25 min readdecision frameworksinterview-grade depth
We will not redefine OOP from scratch. We assume you know classes, methods, fields, inheritance, polymorphism. Today we sharpen the four decisions every backend developer faces — the ones interviewers test and frameworks force on you.

Why OOP at all? — the 30-second story

Before OOP (1980s and earlier), code was organized as functions over data structures (procedural). For a 5,000-line program, fine. For a 500,000-line program, you couldn't tell what depended on what. Changes anywhere broke things everywhere.

OOP's bet: bind data and the operations on it together into "objects." Hide the internals. Let objects delegate to other objects. The result: large programs become tractable because each object is its own little world with a public API.

📌 The single best definition of OOP

OOP is not primarily about inheritance or polymorphism. It is about encapsulation — hiding state behind a public interface so that changes inside one object cannot ripple through the entire codebase. Everything else (inheritance, polymorphism, abstraction) supports this one goal.

Decision 1 · Abstract class vs. Interface

This is the most-asked OOP interview question. Every learner gets a vague answer. Here is the precise one.

Abstract class

  • Use when you have a partial implementation to share + want subclasses to fill in gaps
  • Can have state (instance fields)
  • Can have constructors
  • A class can extend only one abstract class
  • Methods can be protected / private
  • Best for: "is-a" relationships in a single hierarchy

Interface

  • Use to define a capability or contract — what an object can do
  • Cannot hold mutable state (only constants)
  • No constructors
  • A class can implement many interfaces
  • All methods public by default
  • Best for: "can-do" roles, decoupling, mocking

The simple decision rule

  1. Will I share code (real method bodies) or just a contract?
  2. Do classes that satisfy this need to fit into one hierarchy, or play many roles?

Need shared code + single hierarchy → abstract class.
Need a contract + classes might play many roles → interface.

Abstract class · is-aPaymentMethodCardPaymentUPIPaymentshared: validate(), audit()subclass fills: charge()Interface · can-do<<Refundable>>CardPaymentSubscriptionFeecontract: refund(amount)unrelated classes implement
A real-world example — the same domain handled both ways.
🧠 Java 8 changed the line a little

Since Java 8, interfaces can have default methods (with bodies). This blurred the line — but the state rule still holds: only abstract classes can have mutable instance fields. If you need state, you need an abstract class.

Decision 2 · Marker interfaces — what and why

A marker interface is an interface with no methods. Empty. So why have it?

The classic example is java.io.Serializable. It is empty:

public interface Serializable {
  // nothing here
}

Yet putting implements Serializable on your class changes how it's treated by the JVM. Other code (like ObjectOutputStream) checks at runtime: "does this object implement Serializable?" If yes — proceed. If no — throw NotSerializableException.

The interface is a flag, a yes/no marker. Hence "marker."

Common marker interfaces

  • Serializable — "I can be turned into bytes"
  • Cloneable — "you can call clone() on me"
  • RandomAccess — "I support O(1) index access" (used by ArrayList)

Modern alternative — annotations

  • Since Java 5, annotations like @Entity, @Component serve a similar role
  • More flexible: can carry parameters
  • Marker interfaces persist mainly for type safety (compiler can check)
  • For new code, prefer annotations

Decision 3 · Composition over inheritance

This is the most repeated principle in modern OOP. It means: prefer holding an object as a field over extending its class.

Why?

Inheritance creates a permanent, compile-time bond. Once B extends A, every public method of A becomes part of B's API. Change A, you risk breaking B. This is called the fragile base class problem.

Composition (B has a field of type A) is loose. B exposes only what it wants. B can swap A for a different implementation at runtime.

Inheritance — rigid

class Logger {
  void log(String s) {...}
}

class UserService extends Logger {
  // inherits log()
  // also inherits ALL
  // other Logger methods
}

Composition — flexible

class UserService {
  private final Logger logger;

  UserService(Logger l) {
    this.logger = l;
  }
  // expose only what's needed
  // swap logger easily
}
📌 The Spring connection

Spring's entire dependency-injection model is composition taken seriously. @Autowired injects a dependency as a field — never via inheritance. Modern Java backend code uses inheritance sparingly (mostly within a small framework hierarchy) and composition heavily.

Decision 4 · The four pillars — what really matters

Tutorials list four pillars: encapsulation, inheritance, polymorphism, abstraction. Most learners memorize the words. Let me reframe by importance for daily work:

PillarHow often you'll use itWhy it matters
EncapsulationEvery classPrivate fields + getters/setters. Lombok's @Getter/@Setter. The default.
PolymorphismEvery framework methodSpring passes you a List, you don't know if it's ArrayList or something else. That's polymorphism.
AbstractionEvery layerYour service depends on a UserRepository interface, not a concrete class. Lets Spring swap implementations.
InheritanceRarely (in your code)You'll extends framework classes (extends RuntimeException) but not your own often. Composition wins.

Polymorphism — the practical version

"One name, many forms." But what does it mean in code you'll write?

// You declare with the abstract type
List<User> users = userRepository.findAll();

// Spring Data returns... ArrayList? Hibernate's PersistentBag?
// You don't care. You can call .size(), .get(0), .stream()
// because all of them satisfy the List contract.

Polymorphism's daily payoff: your code depends on contracts, not concrete types. If Spring switches the implementation tomorrow, your code keeps working.

Common OOP mistakes I see in learners

Quick refresh — concepts you asked about

Abstract class, abstract method

A class marked abstract cannot be instantiated. It exists to be extended. Methods marked abstract have no body and force subclasses to implement them.

Final class / final method

final on a class = cannot be subclassed. final on a method = cannot be overridden. Used to lock down design intent. String is final — that's why nobody can subclass it.

Static

Belongs to the class, not an instance. Math.max() doesn't need a Math object. Static methods can't access instance fields.

this vs super

this = the current instance. super = the parent class part of the current instance. super.method() calls the parent's version.

Pause & reflect

Lock in today's learning

If any answer is fuzzy, that's the section you re-read.

  1. Give a concrete example where you'd choose an abstract class over an interface, and one for the reverse.
  2. Why is Serializable empty? What does that empty interface actually do?
  3. Explain "composition over inheritance" using your own example.
  4. What does polymorphism let you do in everyday Spring code?
  5. Why is encapsulation the most important OOP pillar — even more than the others?
  6. What problem does the fragile base class describe, and which OOP rule fixes it?

End of Day 3. Tomorrow: Collections internals — the most-used and most-misunderstood part of Java.