Delegated Properties: How by lazy Works Under the Hood

 Deconstructing the bytecode, hidden proxy fields, and the genius of thread-safe double-check locking.

Delegated Properties: How by lazy Works Under the Hood

TL;DR: by lazy is a compiler-generated proxy that hands off property storage to a thread-safe delegate, trading a tiny virtual-call cost for deferred initialization and optimized memory behavior.

1. The Concept: Property vs. Delegate

When you define a delegated property, you are telling the compiler: “Don’t manage the storage for this field yourself. Hand the ‘get’ and ‘set’ logic over to this other object (the delegate).”

class Configuration {
// The 'by' keyword triggers the compiler transformation
val apiKey: String by lazy {
println("Generating API Key...")
"SECRET_12345"
}
}

At first glance, it looks like apiKey is a simple String field. In reality, the backing field is gone, replaced by a proxy that executes logic upon access.

2. The Bytecode Transformation: The Hidden Proxy

When you compile the code above, the Kotlin compiler generates a hidden “delegate” field and a static array of property metadata.

The Decompiled “Pseudo-Java” Reality:

public final class Configuration {
// 1. Static array holding KProperty metadata
static final KProperty[] $$delegatedProperties = {
Reflection.property1(new PropertyReference1Impl(Configuration.class, "apiKey", ...))
};

// 2. The hidden field for the delegate.
// The initializer is compiled into a synthetic lambda class.
private final Lazy apiKey$delegate = LazyKt.lazy(new Configuration$apiKey$2());

@NotNull
public final String getApiKey() {
// 3. Redirection to the delegate's getValue()
return (String)this.apiKey$delegate.getValue(this, $$delegatedProperties[0]);
}
}

The metadata in $$delegatedProperties enables reflection (like ::apiKey.name) and tooling support, even though the original property no longer has a backing field. While this involves an extra virtual call, the cost is negligible compared to the heavy initialization it usually wraps.

3. Deep Dive: Inside SynchronizedLazyImpl

The lazy function returns an instance of Lazy<T>. By default, Kotlin uses LazyThreadSafetyMode.SYNCHRONIZED. Inside the standard library, it implements a thread-safe Double-Check Locking pattern.

override val value: T
get() {
val v1 = _value
// Initial check (no lock) for performance
if (v1 !== UNINITIALIZED_VALUE) return v1 as T

return synchronized(lock) {
val v2 = _value
// Second check (inside lock) to ensure single initialization
if (v2 !== UNINITIALIZED_VALUE) {
v2 as T
} else {
val typedValue = initializer!!()
_value = typedValue
initializer = null // Memory safety: prevents leaking captured variables
typedValue
}
}
}

4. Choosing the Right Tool

Press enter or click to view image in full size
Choosing the Right Tool
Choosing the Right Tool

5. Performance and Scaling

Delegated properties are a compiler-level feature, not a JVM one. This abstraction allows for incredible Separation of Concerns. I recently used a custom delegate to bridge SharedPreferences. The UI code looks like it is touching a local variable, but the persistence logic is hidden entirely within a reusable delegate class.

When NOT to use by lazy: Avoid it for properties that are extremely small and accessed in tight, high-performance loops (millions of times per second). The overhead of the getValue virtual call and potential synchronization checks can add up if the initialization logic itself is trivial.

🙋‍♂️ Frequently Asked Questions (FAQs)

Does by lazy increase app startup time?

Actually, it usually improves it. By deferring the initialization of heavy objects until they are actually accessed, your initial class loading is lighter.

What is LazyThreadSafetyMode.NONE?

If a property is only accessed from a single thread (like the Android Main Thread), NONE removes the synchronized overhead, making access slightly faster.

Can I use by lazy with var?

No. lazy is strictly for val. For a var that is initialized late, use lateinit var or create a custom ReadWriteProperty delegate.

Understanding delegated properties at this level makes it easier to reason about performance, memory, and API design — especially in large, complex Kotlin codebases.

💬 Join the Conversation!

  • Have you ever checked your $$delegatedProperties array in a decompiler?
  • What is your favorite custom delegate: View Binding, SharedPreferences, or something custom?
  • lateinit var vs by lazy—which do you reach for first when designing a class?

📘 Master Your Next Technical Interview

Since Java is the foundation of Android development, mastering DSA is essential. I highly recommend “Mastering Data Structures & Algorithms in Java”. It’s a focused roadmap covering 100+ coding challenges to help you ace your technical rounds.

Comments

Popular posts from this blog

No More _state + state: Simplifying ViewModels with Kotlin 2.3

Why You Should Stop Passing ViewModels Around Your Compose UI Tree 🚫

Is Jetpack Compose Making Your APK Fatter? (And How to Fix It)