The “Downcast Trap” in Kotlin’s Explicit Backing Fields

Why eliminating the double-property boilerplate changes how we protect our architecture

Image generated by AI

Disclosure: This article was drafted by me and refined with the help of AI tools.

If you have written Kotlin in the last few years, you are intimately familiar with the double-property boilerplate. Whether in Android ViewModels or general state holders, maintaining a private mutable property alongside a public read-only property is a chore we have all accepted in the name of strict encapsulation to prevent our internal state from being hijacked by outside classes.

It makes the code significantly cleaner (and slightly more memory efficient). However, it fundamentally changes how we protect our state, moving from a physical object boundary to a simple type restriction. Let’s look at the tradeoff.

The Old Way: Wrapper Protection

For years, the standard approach to encapsulating state has looked like this:

class OldViewModel {
// 1. The private mutable state
private val _uiState = MutableStateFlow(UiState())

// 2. The public read-only state
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
}

This is tedious to write, but it provides a strict architectural guarantee. When you call .asStateFlow(), Kotlin does not just change the type; it creates a brand new wrapper object in memory (ReadonlyStateFlow). While this physical barrier is fantastic for safety, it does mean you are incurring a minor memory allocation overhead by creating a secondary wrapper object for every exposed state.

The New Way: Upcasting

Explicit Backing Fields allow you to merge these two properties into one concise declaration, bypassing that extra memory allocation entirely:

class NewViewModel {
val uiState: StateFlow<UiState>
field = MutableStateFlow(UiState())
}

Inside your class, the Kotlin compiler smart-casts the field so you can mutate it internally. Outside the class, the compiler restricts callers to the read-only StateFlow interface.

It looks incredibly clean and saves an allocation, but it introduces a subtle architectural vulnerability.

The Downcast Trap

Because Explicit Backing Fields do not create a wrapper object, the underlying object in memory is still a MutableStateFlow. You are relying entirely on the compiler’s upcasting to hide the mutability.

If a consumer outside of your class decides to aggressively downcast your state, the behaviors of these two approaches are drastically different:

// Scenario A: The Old Way
// ❌ Fails with ClassCastException at runtime
(oldViewModel.uiState as MutableStateFlow).value = BadState()

// Scenario B: The New Way
// ⚠️ Silently succeeds and corrupts your internal state
(newViewModel.uiState as MutableStateFlow).value = BadState()

With the old way, the ClassCastException acts as a physical barrier preventing architectural violations. With Explicit Backing Fields, a rogue downcast will succeed, and your internal state can be silently mutated from the outside, bypassing your business logic entirely.

The Pragmatic Reality: Is it a dealbreaker?

Is this a reason to avoid Explicit Backing Fields? For most application development teams, no.

We already accept this exact same tradeoff with standard Kotlin collections. Exposing a List that is secretly a MutableList under the hood is a standard practice, and it relies on the exact same upcasting mechanism.

You should not design your internal application architecture around the assumption that your teammates are actively writing malicious downcasts to bypass your interfaces. That is what Code Reviews and Static Analysis tools (like Detekt or SonarQube) are for.

The Exception: If you are writing a closed-source SDK or a public library where you cannot trust the consuming application, you should absolutely stick to defensive wrappers like .asStateFlow(). In a public API surface, physical boundaries matter more than saving a micro-allocation.

Are you planning to adopt Explicit Backing Fields in your codebase, or do you prefer the strict wrapper protection of the old approach? Let me know your thoughts in the comments!


The “Downcast Trap” in Kotlin’s Explicit Backing Fields was originally published in ProAndroidDev on Medium, where people are continuing the conversation by highlighting and responding to this story.