Interfaces and inheritance
Classes rarely stand alone. Sometimes several classes should share a common shape (every payment
method can charge); sometimes one class is a specialised version of another (a SavingsAccount is
an Account). Kotlin gives you interfaces and inheritance for these, with a crucial default
that differs from most languages: classes are closed to inheritance unless you open them.
Interfaces — a shared contract
An interface declares what a type can do, without saying how:
interface PaymentMethod {
fun charge(amountPaise: Int): Boolean // no body — just the contract
val displayName: String
}
Any class that implements the interface must provide those members:
class UpiPayment(val vpa: String) : PaymentMethod {
override val displayName = "UPI ($vpa)"
override fun charge(amountPaise: Int): Boolean {
println("Charging ₹${amountPaise / 100} via UPI to $vpa")
return true
}
}
class CardPayment(val last4: String) : PaymentMethod {
override val displayName = "Card ****$last4"
override fun charge(amountPaise: Int): Boolean {
println("Charging ₹${amountPaise / 100} to card ending $last4")
return true
}
}
The : PaymentMethod says "this class implements the interface", and override is required on
each member that fulfils the contract — Kotlin makes you state your intent, so you cannot fulfil an
interface by accident or misspell a method name and not notice.
The payoff is that code can work with any payment method through the interface, not knowing which concrete class it has — polymorphism:
fun checkout(method: PaymentMethod, amountPaise: Int) {
println("Paying with ${method.displayName}")
method.charge(amountPaise)
}
checkout(UpiPayment("kavita@okaxis"), 45000)
checkout(CardPayment("1234"), 45000)
checkout takes a PaymentMethod and calls charge — it works with UPI, card, or any future
method you add, without changing. This is the single most valuable use of interfaces: program to
the contract, not the concrete type, so new implementations slot in without touching existing code.
Interfaces can have default implementations
Unlike older Java, a Kotlin interface method can have a body — a default implementation that implementers inherit unless they override it:
interface PaymentMethod {
fun charge(amountPaise: Int): Boolean
val displayName: String
fun describe(): String = "Payment via $displayName" // default — free for all implementers
}
Every implementer gets describe() without writing it, and can override it if needed. Use defaults
for behaviour that is the same across most implementers, keeping the classes lean.
Inheritance — and why classes are final by default
Inheritance lets one class extend another, reusing and specialising its behaviour. But there is a
deliberate friction: Kotlin classes are final (closed) by default — you cannot inherit from one
unless it is marked open:
open class Account(val id: String, protected var balancePaise: Int) {
open fun describe(): String = "Account $id: ₹${balancePaise / 100}"
}
class SavingsAccount(id: String, balance: Int, val rate: Double) : Account(id, balance) {
override fun describe(): String = "${super.describe()} (savings @ $rate%)"
}
Read the design decisions:
open class Account— the class must beopento be inherited; a plainclasscannot be extended.open fun describe()— a method must beopento be overridden; methods are alsofinalby default.: Account(id, balance)— the subclass calls the superclass constructor.override— required to override, as with interfaces.super.describe()— calls the parent's version, soSavingsAccountextends rather than replaces it.
Why closed by default? Because inheritance is a strong coupling that is easy to get wrong.
Extending a class that was not designed to be extended is a classic source of fragile code — a
change in the parent silently breaks the child. Kotlin makes you opt in to allowing inheritance,
which forces the class author to think "is this safe to extend?" and design for it. It is the same
philosophy as val and read-only collections: the safe, restrictive choice is the default.
Prefer interfaces and composition to inheritance
Here is the guidance that matters more than the mechanics. Reach for interfaces first, and
inheritance sparingly. Deep inheritance hierarchies — A extends B extends C extends D — become
rigid and hard to follow; a change high up ripples unpredictably downward. The modern, better-tested
approach is:
- Interfaces to share a contract (many classes, one shape).
- Composition — a class holds another and delegates to it — to share behaviour, rather than inheriting it.
Kotlin even has first-class support for composition-over-inheritance with the by keyword
(delegation), which the design-patterns module covers. The rule of thumb: use inheritance only for a
genuine "is-a-specialised-kind-of" relationship where the subtype truly is the supertype; for
everything else — sharing a contract, reusing behaviour — reach for an interface or composition.
Junior developers over-use inheritance; experienced ones use it rarely and deliberately.
Check your work
What an interface declares. A contract — what a type can do — without (necessarily) how.
What override does, and why it is required. Marks a member as fulfilling a contract or replacing
a parent's; required so you cannot do it by accident or by a misspelling.
What polymorphism buys you. Code works with any implementation through the interface, so new implementations slot in without changing existing code.
What a default implementation is. An interface method with a body, inherited unless overridden.
Why Kotlin classes are final by default. Inheritance is strong coupling that is easy to get
wrong; opting in forces the author to design for extension.
What must be open. A class to be inherited, and a method to be overridden.
What super.method() does. Calls the parent's version, so a subclass can extend rather than
replace.
The guidance on inheritance versus interfaces/composition. Prefer interfaces and composition; use inheritance only for a genuine "is-a-specialised-kind-of" relationship.
Practice
- Write a
PaymentMethodinterface and two implementing classes. Useoverrideon each member. - Write a
checkout(method: PaymentMethod, ...)function and call it with both implementations. Note it does not know which concrete class it has. - Add a default
describe()to the interface and confirm both classes get it for free. Override it in one. - Try to inherit from a plain (non-
open) class and read the compiler error. Then mark itopen. - Write
open class Accountand aSavingsAccountsubclass whosedescribe()callssuper.describe(). - Try to override a method that is not
openand read the error. - Take a design you would instinctively model with inheritance and reconsider whether an interface or composition fits better. Write down which and why.
Official documentation
- Kotlin — Interfaces — Contracts, default implementations, multiple interfaces.
- Kotlin — Inheritance —
open,override,super, and why classes are final by default. - Kotlin — Delegation — Composition with
by, the alternative to inheritance.
Next: sealed classes and exhaustive when — a Kotlin highlight.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship