Kotlin : Singleton with Memory Efficiency

Romman SabbirRomman Sabbir
4 min read

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

ApproachLazy InitializationThread SafetyMemory EfficientManual DestructionPerformance
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 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…

1
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.