Kotlin : Singleton with Memory Efficiency
Dive into the world of memory-efficient singleton design in Kotlin

Senior Android Engineer from Bangladesh. Love to contribute in Open-Source. Indie Music Producer.
Singletons are a fundamental design pattern in software development, ensuring that a class has only one instance and provides a global point of access to it. In Kotlin, implementing a singleton is straightforward using the object keyword, but what if we need a memory-efficient, lazy-loaded, and thread-safe singleton?
In this article, we'll explore different ways to implement a singleton in Kotlin while optimizing memory usage and lazy initialization. We'll compare eager vs. lazy instantiation, discuss potential pitfalls like unnecessary memory consumption, and implement a double-checked locking singleton, ensuring both efficiency and performance. Plus, we'll introduce a way to destroy the singleton instance, giving you more control over resource management.
By the end, you'll know which approach best suits your needs, whether you're building an Android app, a back-end service, or a high-performance Kotlin application.
Let's dive into the world of memory-efficient singleton design in Kotlin with some practical examples!
Using Lazy Delegation
If we want a lazy-loaded singleton, Kotlinβs lazy delegation makes it incredibly easy:
class Singleton private constructor() {
init {
println("Singleton instance created")
}
companion object {
val instance: Singleton by lazy { Singleton() }
}
fun doSomething() {
println("Doing something...")
}
}
β Why use this?
The instance is created only when accessed for the first time.
Thread-safe by default.
Thread-Safe Lazy Singleton (Synchronized)
If we need a thread-safe singleton with explicit control, we can use synchronized to ensure only one instance is created, even in multi-threaded environments.
class SafeSingleton private constructor() {
init {
println("SafeSingleton instance created")
}
companion object {
@Volatile
private var instance: SafeSingleton? = null
fun getInstance(): SafeSingleton {
return instance ?: synchronized(this) {
instance ?: SafeSingleton().also { instance = it }
}
}
}
}
β Why use this?
Thread-safe.
Lazy initialization.
Prevents multiple instances from being created in multi-threaded scenarios.
πΉ Note: @Volatile ensures that updates to instance are visible across threads. However, in some cases, @Volatile might cause the object to load on the main thread if accessed from the UI. While this is usually not an issue, we can manually handle initialization in a background thread if needed.
Kotlin object Singleton (Best for Simplicity)
If we donβt need lazy initialization or additional control, Kotlin provides a built-in way to create singletons using the object keyword:
object SimpleSingleton {
fun doSomething() {
println("Doing something in SimpleSingleton")
}
}
β Why use this?
Short and simple.
Thread-safe by default.
Easy to use for lightweight singletons.
β Downside?
- Eager initialization (created at class load time, even if never used).
Simple Lazy Singleton Without synchronized
If thread safety isnβt a concern, a basic lazy singleton without synchronization can be used:
class SimpleSingleton private constructor() {
init {
println("SimpleSingleton instance created")
}
companion object {
private var instance: SimpleSingleton? = null
fun getInstance(): SimpleSingleton {
if (instance == null) {
instance = SimpleSingleton()
}
return instance!!
}
}
}
β Why use this?
Works fine in a single-threaded environment.
Saves memory compared to an eager singleton.
β Downside?
- Not thread-safe.
Optimized Singleton with Memory Efficiency & Manual Destruction
If we need a fully optimized, thread-safe, and lazy-loaded singleton, while also allowing manual destruction, the double-checked locking singleton is the best approach:
class OptimizedSingleton private constructor() {
init {
println("OptimizedSingleton instance created")
}
companion object {
@Volatile
private var instance: OptimizedSingleton? = null
fun getInstance(): OptimizedSingleton {
return instance ?: synchronized(this) {
instance ?: OptimizedSingleton().also { instance = it }
}
}
fun destroyInstance() {
synchronized(this) {
instance = null
println("OptimizedSingleton instance destroyed")
}
}
}
fun doSomething() {
println("Doing something in OptimizedSingleton")
}
}
β Why use this?
Lazy initialization (created only when needed).
Thread-safe (using
synchronized).Efficient memory usage (not pre-loaded).
Manual destruction (
destroyInstance()) allows explicit cleanup if needed.
Comparing Singleton Approaches
| Approach | Lazy Initialization | Thread Safety | Memory Efficient | Manual Destruction | Performance |
object Singleton | β (Eager) | β Yes | β (Always Loaded) | β No | β Fast |
| Simple Companion Object | β Yes (On Demand) | β No | β Yes | β No | β Fast |
Double-Checked Locking | β Yes (On Demand) | β Yes | β Best | β Yes | β Best |
When to Use Which Singleton?
Use
objectSingleton β When simplicity and quick access are preferred.Use Simple Lazy Singleton β If running in a single-threaded environment.
Use
Double-Checked Lockingβ When memory efficiency, lazy initialization, and thread safety are critical.Use
OptimizedSingletonβ If we need all the benefits ofDouble-Checked Lockingplus manual destruction.
By choosing the right approach, we can optimize both memory usage and performance while keeping our singleton efficient and scalable.
Thatβs it for today. Happy Codingβ¦




