Kotlin : Singleton with Memory Efficiency


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
object
Singleton → 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 Locking
plus 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…
Subscribe to my newsletter
Read articles from Romman Sabbir directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Romman Sabbir
Romman Sabbir
Senior Android Engineer from Bangladesh. Love to contribute in Open-Source. Indie Music Producer.