Module 22 · OOP in practice · Lesson 3 of 4
Interfaces vs abstract classes
Two ways to define a type others implement. What each can hold, single versus multiple inheritance, default methods and how they blur the line, how each evolves when you add a method, and a short rule for choosing.
Why it matters
"When would you use an abstract class instead of an interface?" is one of the most common object-oriented interview questions, and the textbook answer from 2005 is out of date. Default methods in Java and C# have blurred the line, and Go, Rust and TypeScript use different models entirely. What still matters is what each lets a type hold, how many a class can have, and what happens when you add a method later and a hundred implementations already exist.
The idea
Both define a type that other classes implement or extend, and both can declare methods without implementations. They differ in what else they can carry and how many a class can take.
| Capability | Interface | Abstract class |
|---|---|---|
| Abstract methods | Yes | Yes |
| Method implementations | Default and static methods (Java 8+, C# 8+) | Yes, any |
| Instance state (fields) | No; constants only | Yes |
| Constructors | No | Yes, to initialise shared state |
| Non-public members | Limited (private helpers in Java 9+) | protected and private, freely |
| How many a class can have | Many | One (in Java and C#) |
| Expresses | A capability or role: Comparable, Closeable, PaymentGateway | A partial implementation of a family: a base with shared state and a fixed algorithm |
Evolving each one
Adding an abstract method to a published interface breaks every implementer, because each must now provide it. Default methods were added to Java and C# precisely to evolve interfaces without breaking implementations: the new method ships with a body that implementations inherit until they override it. Abstract classes have always allowed this, since a new concrete method is simply inherited. The difference that remains is state: a default method can only be written in terms of the interface's other methods, while an abstract class can use fields.
The abstract class's real job
Abstract classes shine for the template method pattern: the base class fixes the steps of an algorithm and the shared state, and subclasses fill in specific steps. An exporter that always opens a destination, writes chunks with retries and closes cleanly, where only "write a chunk" varies, is a natural abstract base. The cost is the single-parent slot and the fragile-base-class coupling from the inheritance lesson.
Other languages, other models
| Language | Model | Effect |
|---|---|---|
| Go | Implicit, structural interfaces; no inheritance | Small interfaces defined by consumers; reuse by embedding structs |
| Rust | Traits with default methods; no inheritance of data | Behaviour is composed from traits; shared data by composition |
| TypeScript | Structural interfaces and abstract classes | Interfaces describe shapes, erased at runtime; abstract classes exist at runtime |
| Python | Abstract base classes and Protocols | ABCs enforce methods at instantiation; Protocols type-check structurally |
| Scala, Kotlin | Traits or interfaces with state-free defaults, sealed hierarchies | Mixins of behaviour; closed hierarchies for pattern matching |
The trend across modern languages is clear: behaviour comes from interfaces or traits, data and reuse come from composition, and implementation inheritance is used sparingly.
A short rule
- 1
Default to an interface
It defines the contract, leaves implementers free to extend anything else, and is easy to fake in tests. - 2
Add an abstract base class alongside it when implementations share real state or a fixed algorithm
Many libraries ship both: an interface for the contract and an optional abstract skeleton that implements the boring parts. - 3
Use default methods for convenience and evolution, not to smuggle in behaviour that needs state
If a default needs fields, it belongs in a class. - 4
Seal what you do not intend others to extend
Sealed interfaces or classes, or final classes, make the hierarchy closed and the design explicit.
Trade-offs
Interfaces give flexibility, multiple roles and easy testing, at the cost of no shared state. Abstract classes give shared state, constructors and a fixed skeleton, at the cost of using up the only parent and coupling subclasses to the base. Default methods narrow the gap and add their own risk: two interfaces with the same default force a class to resolve the conflict explicitly.
In an interview
Answer with both the rule and the reason: "PaymentGateway is an interface because it is a role with several unrelated implementations and we fake it in tests. Underneath, an abstract HttpGateway base holds the HTTP client, retry policy and signing, and each provider extends it and implements only the request mapping. If we need to add a method to the interface later, it ships as a default to avoid breaking third-party implementations."
Check yourself
A library adds a new abstract method to a public interface that thousands of external classes implement. Their builds break on upgrade. What would have avoided this?