skip to content

What are the design and concurrency pitfalls of putting mutable state in an `object` singleton, and how do you mitigate them?

level: seniorimportance: should knowfreq 45%

answer

  1. Object = process-wide global, long-lived, shared
  2. Init is safe; fields/methods are NOT — use atomics/concurrent/locks
  3. State leaks across tests; hide behind interface + DI
  4. Never GC'd → cache/listener/Context leaks
  5. Prefer stateless objects; inject a class when state is needed

basics

~20 s

A singleton object is shared by the whole app, so mutable data in it is global state. Many threads can change it at once, causing bugs, and it makes tests harder. Keep objects stateless or guard their state.

solid answer

~50 s

An `object` is process-wide global state. Three pitfalls follow. (1) **Concurrency:** initialization is thread-safe but the object's *fields and methods are not*; mutable `var`s or plain collections race. Mitigate with atomics (`AtomicInteger`/`AtomicReference`), concurrent collections (`ConcurrentHashMap`), `@Synchronized`/locks, or by storing immutable snapshots in a `@Volatile` reference / `StateFlow`. (2) **Testability:** a singleton holding state leaks between tests and can't be swapped for a fake; prefer injecting an interface and binding the object (or a class) via DI. (3) **Lifecycle/memory:** an object lives for the whole JVM, so anything it references (caches, listeners) never gets GC'd — a leak risk, and on platforms like Android it can outlive a Context. Best practice: keep objects **stateless** (pure helpers, constants, registries of immutable data); when state is unavoidable, make it explicitly thread-safe and consider whether a regular injected class is the better tool.

code

kotlin · 10 lines
kotlin
// Race-free counter in a singleton
object Metrics {
    private val requests = java.util.concurrent.atomic.AtomicLong()
    private val byPath = java.util.concurrent.ConcurrentHashMap<String, Long>()
    fun onRequest(path: String) {
        requests.incrementAndGet()
        byPath.merge(path, 1L, Long::plus)
    }
    fun total(): Long = requests.get()
}

go deeper

for a junior

Recognizes a singleton is shared and shouldn't hold unguarded mutable data.

for a middle

Names concrete fixes (atomics, concurrent collections, @Synchronized).

for a senior

Connects concurrency, testability, and lifecycle/leak concerns and gives a stateless-vs-inject decision guide.

for a principal

Weighs singleton convenience against DI, coupling/SRP, and platform lifecycle, choosing the right tool per context.

## The core problem: an object is global, long-lived, shared A singleton `object` exists once for the whole JVM and is reachable from anywhere. That makes it convenient and dangerous in equal measure. ## Pitfall 1 — Concurrency on the object's own state *Initialization* is thread-safe (one-time class init). **Runtime state is not.** This races: ```kotlin object Stats { var hits = 0 // NOT thread-safe val seen = mutableListOf<String>() // NOT thread-safe fun record(x: String) { hits++; seen += x } } ``` Fixes: - **Atomics:** `AtomicInteger`, `AtomicLong`, `AtomicReference` for single values. - **Concurrent collections:** `ConcurrentHashMap`, `Collections.synchronizedList`, or copy-on-write. - **Locks / `@Synchronized`:** guard compound updates. - **Immutable snapshot in a `@Volatile var`**, or coroutine-friendly **`StateFlow`/`MutableStateFlow`** to publish state safely. ```kotlin object Stats { private val hits = java.util.concurrent.atomic.AtomicInteger() private val seen = java.util.concurrent.ConcurrentHashMap.newKeySet<String>() fun record(x: String) { hits.incrementAndGet(); seen += x } } ``` ## Pitfall 2 — Testability State inside a singleton **persists across tests** (no per-test reset) and **can't be substituted** with a fake. This produces order-dependent, flaky tests. Mitigation: - Hide the object behind an **interface** and depend on the interface, injecting the object (or a test double) via constructor/DI. - Keep any resettable state behind a method you can call in `@BeforeEach`, or avoid state entirely. ```kotlin interface Clock { fun now(): Long } object SystemClock : Clock { override fun now() = System.currentTimeMillis() } class Service(private val clock: Clock) { /* inject SystemClock or a fake */ } ``` ## Pitfall 3 — Lifecycle & memory The object never gets garbage-collected, so whatever it references is retained for the JVM's life. Caches grow unbounded; registered listeners leak. On Android, holding a `Context`/`Activity` in an object is a classic memory leak because the object outlives the UI. ## Pitfall 4 — Hidden coupling / SRP Because anything can call `Singleton.x`, dependencies become invisible (no constructor signal), encouraging god-objects and tight coupling. ## Decision guide - **Great fit:** stateless helpers, pure functions, immutable constants, a registry of immutable data, an interface implementation with no mutable state. - **Reconsider:** anything with mutable, request/lifecycle-scoped, or test-sensitive state — prefer a regular class injected by your DI container (in Spring, a bean). ## Keywords/APIs to name `AtomicInteger`/`AtomicReference`, `ConcurrentHashMap`, `@Synchronized`, `@Volatile`, `StateFlow`/`MutableStateFlow`, dependency injection, garbage collection / `Context` leaks.

  • Initialization is thread-safe, so why isn't `var hits = 0; fun inc() { hits++ }` safe?
    Only the one-time construction is guarded. `hits++` is a read-modify-write that interleaves across threads; use `AtomicInteger` or a lock.
  • Why can a singleton object cause a memory leak?
    It is never garbage-collected, so anything it holds (caches, listeners, an Android Context) is retained for the JVM's lifetime.

A singleton with mutable state is a shared whiteboard in a busy office — handy, but everyone scribbles on it at once and nobody erases it between meetings.

saying these in an interview costs you the question

  • Assuming the object's methods are thread-safe because init is
  • Putting request/lifecycle-scoped mutable state in an object
  • Holding an Android Context/Activity in an object
  • No plan for testing/reset of singleton state
  • Using objects as a global grab-bag, hiding dependencies

context