Companion Objects vs. Singletons in Kotlin: A No-Nonsense Guide with a Cosmic Twist
Kotlin is packed with features that make coding cleaner and more intuitive. Two concepts that often spark confusion, even among seasoned developers, are companion objects and singletons. Let’s demystify them with a fresh perspective, zero jargon, and a quirky example you won’t find anywhere else.


Companion Objects: Your Class’s Personal Assistant
A companion object is like a loyal sidekick tied to a specific class. It can access the class’s private members (yes, even the secrets!) and is ideal for housing methods or properties that belong to the class but don’t need a separate instance.
How to Spot One
Declare it inside a class with the companion keyword:
class Spaceship(private val engineSerial: String) {
// Companion object
companion object BlackBox {
fun logLaunch(serial: String) {
println("Spaceship with engine $serial launched! 🚀")
}
}
fun launch() {
BlackBox.logLaunch(engineSerial) // Access private engineSerial
}
}
Here, BlackBox is a companion object inside the Spaceship class. It logs launches using the private engineSerial, something only a companion can do.
Why Use a Companion?
-
Access Private Data: It’s the only “object” that can peek into a class’s private properties.
-
Class-Specific Logic: Perfect for factory methods (Spaceship.create()) or constants tied to the class.
-
No Duplicate Instances: There’s only one companion per class, so no memory bloat.
Singletons: The Global Supervisors
A singleton in Kotlin is a globally accessible, single instance of an object. Declare it using object (not inside a class!), and it lives independently.
How to Spot One
object CentralComputer {
private val spaceships = mutableListOf<String>()
fun trackSpaceship(serial: String) {
spaceships.add(serial)
println("Now tracking ${spaceships.size} spaceships.")
}
}
The CentralComputer singleton tracks all spaceships across your app. Any class can call CentralComputer.trackSpaceship(...), and there’s only one instance managing the list.
Why Use a Singleton?
-
Global Access: Need a shared resource (like a database connection)? Singletons are your friend.
-
Lazy Initialization: Created only when first used, saving memory.
-
Stateless Services: Great for logging, analytics, or hardware control.
Companion vs. Singleton: The Cosmic Showdown
Aspect | Companion Object | Singleton
---------------------|-------------------------------------------|-----------------------------------------
Scope | Tied to a class (like Spaceship.BlackBox) | Standalone (CentralComputer)
Private Access | Can access private members of its class | Can’t unless nested inside the class
Initialization | Created when the class is loaded | Created lazily on first use
Use Case | Class-specific factories, constants | App-wide resources, shared state
When to Use Which?
- Companion Objects Are Better When…
-
You need a factory method for a class (e.g., Spaceship.fromConfig()).
-
Storing constants specific to a class (e.g., Spaceship.MAX_SPEED).
-
Accessing private properties of the class (like logging internal data).
2. Singletons Shine When…
-
Managing app-wide resources (e.g., a network client or database).
-
You need a single source of truth (e.g., a user session manager).
-
You want lazy initialization to optimize performance.
A Unique Example: Spaceships and Cosmic Control
Let’s combine both concepts in a real-world scenario:
// Singleton: Manages all spaceships in the universe
object UniverseRegistry {
private val allShips = mutableListOf<Spaceship>()
fun register(ship: Spaceship) {
allShips.add(ship)
println("Universe now has ${allShips.size} spaceships!")
}
}
class Spaceship(private val engineSerial: String) {
init {
UniverseRegistry.register(this) // Singleton in action!
}
// Companion: Handles launch logic for individual ships
companion object BlackBox {
fun launch(ship: Spaceship) {
println("Engine ${ship.engineSerial} firing!")
}
}
fun launch() {
BlackBox.launch(this)
}
}
// Example Usage
val ship1 = Spaceship("X-101")
val ship2 = Spaceship("X-202")
ship1.launch() // Output: Engine X-101 firing!
Here, UniverseRegistry (singleton) tracks all ships globally, while BlackBox (companion) handles ship-specific launches.
Final Thoughts
-
Companion objects are your class’s trusted ally, handling intimate tasks.
-
Singletons are the overseers, managing cross-cutting concerns.
Next time you’re coding in Kotlin, ask: “Does this belong to a single class, or is it a global service?” The answer will guide you to the right tool.
