If you’ve been writing object-oriented code for a while, you’ve probably used inheritance a lot. It feels natural. You create a base class, extend it, override a few methods, and move on.
But as projects grow, inheritance often becomes hard to manage. Classes get tightly coupled. Changes ripple through the codebase. Small tweaks break unexpected things.
This is where Composition Over Inheritance comes in.
In this post, we’ll break down what Composition Over Inheritance really means, why it matters, and how it’s used naturally in modern Kotlin development, especially with Jetpack Compose.
What Does “Composition Over Inheritance” Mean?
Composition Over Inheritance is a design principle that says:
Prefer building classes by combining smaller, reusable components instead of extending base classes.
In simpler terms:
- Inheritance says “is a”
- Composition says “has a”
Instead of forcing behavior through class hierarchies, you compose behavior by using other objects.
A Simple Real-World Example
Think of a smartphone.
A smartphone has a camera, battery, speaker, and screen.
It does not inherit from Camera, Battery, or Speaker.
That’s composition.
If you used inheritance here, the design would fall apart fast.
The Problem With Inheritance
Inheritance looks clean at first, but it comes with hidden costs.
Example Using Inheritance (Problematic)
open class Vehicle {
open fun move() {
println("Vehicle is moving")
}
}
open class Car : Vehicle() {
override fun move() {
println("Car is driving")
}
}
class ElectricCar : Car() {
override fun move() {
println("Electric car is driving silently")
}
}At first glance, this seems fine.
But now imagine:
- You want a flying car
- You want a boat-car
- You want a self-driving electric truck
Your inheritance tree explodes.
Changes to Vehicle affect every subclass. You’re locked into decisions you made early, often before requirements were clear.
This is exactly what Composition Over Inheritance helps you avoid.
Composition Over Inheritance Explained With Kotlin
Let’s rewrite the same idea using composition.
Create Small, Focused Behaviors
interface Engine {
fun move()
}class GasEngine : Engine {
override fun move() {
println("Driving using gas engine")
}
}class ElectricEngine : Engine {
override fun move() {
println("Driving silently using electric engine")
}
}Each class has one clear responsibility.
Compose the Behavior
class Car(private val engine: Engine) {
fun drive() {
engine.move()
}
}Now the Car has an engine, instead of being forced into a rigid hierarchy.
Use It
fun main() {
val electricCar = Car(ElectricEngine())
electricCar.drive()
val gasCar = Car(GasEngine())
gasCar.drive()
}
Output:
Driving silently using electric engine
Driving using gas engine
This is Composition Over Inheritance in action.
Why Composition Over Inheritance Is Better
Here’s why modern Kotlin developers strongly prefer this approach.
1. Less Coupling
Your classes depend on interfaces, not concrete implementations.
2. Easier Changes
You can swap behaviors without rewriting class hierarchies.
3. Better Testability
You can inject fake or mock implementations easily.
4. Cleaner Code
Smaller classes. Clear responsibilities. Fewer surprises.
Composition Over Inheritance in Jetpack Compose
Jetpack Compose is built almost entirely on Composition Over Inheritance.
That’s not an accident.
Traditional UI (Inheritance-Heavy)
class CustomButton : Button {
// override styles, behavior, states
}This leads to rigid UI components that are hard to reuse.
Compose Way (Composition First)
@Composable
fun MyButton(
text: String,
onClick: () -> Unit
) {
Button(onClick = onClick) {
Text(text)
}
}Here’s what’s happening:
MyButtonis not extendingButton- It uses
Button - Behavior is passed in, not inherited
This is Composition Over Inheritance at the UI level.
Why Compose Feels Easier to Work With
Compose avoids deep inheritance trees entirely.
Instead:
- UI is built from small composable functions
- Each function does one thing
- You combine them like building blocks
That’s composition by design.
Delegation: Kotlin’s Built-In Support for Composition
Kotlin makes Composition Over Inheritance even easier with delegation.
Example Using Delegation
interface Logger {
fun log(message: String)
}
class ConsoleLogger : Logger {
override fun log(message: String) {
println(message)
}
}
class UserService(private val logger: Logger) : Logger by loggerNow UserService automatically uses ConsoleLogger’s implementation without inheritance.
This keeps your code flexible and clean.
When Should You Still Use Inheritance?
Inheritance is not evil. It’s just often overused.
Inheritance works best when:
- There is a true “is-a” relationship
- The base class is stable
- You control both parent and child classes
If those conditions are missing, Composition Over Inheritance is usually the safer choice.
Conclusion
Let’s wrap it up.
- Composition Over Inheritance means building behavior using objects, not class hierarchies
- Kotlin makes composition easy with interfaces and delegation
- Jetpack Compose is a real-world example of this principle done right
- Composition leads to flexible, testable, and maintainable code
If you’re writing Kotlin today, especially with Compose, you’re already using Composition Over Inheritance whether you realized it or not.
And once you start designing with it intentionally, your code gets simpler, not harder.
