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.
Search for a command to run...
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.
No comments yet. Be the first to comment.
A practical guide to choosing the right coroutine primitive for your Android/Kotlin projects

This guide explains what Channels are, when to use them, how to use them correctly, and when NOT to use them β in simple, professional language. 1. What problem do Channels solve? In Kotlin coroutines, you often have multiple coroutines running at t...

Learn how to build preview-safe ViewModels in Jetpack Compose using Hilt.

HandlerInterceptor vs Filter in Spring Boot: A Practical Guide with JNDI Injection Prevention

Simple In-Memory Caching: A Tiny Trick with Massive Impact

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!
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.
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.
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?
synchronizedIf 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?
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.
| 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 |
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 of Double-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β¦