Kotlin Delegated Properties: Reduce Boilerplate Code
Kotlin, known for its concise syntax and powerful features, simplifies common programming tasks. One such feature is Kotlin Delegated Properties. Delegated properties in Kotlin allow you to delegate the responsibility of property access and storage to another object. This can significantly reduce boilerplate code while enhancing flexibility and functionality.
In this tutorial, we’ll explore how delegated properties work, when to use them, and examine practical examples that showcase their versatility.

What Are Kotlin Delegated Properties?
Kotlin delegated properties are one of the language’s most powerful yet under-appreciated features. They allow you to extract common property behaviors and reuse them across different classes, following the delegation pattern. Instead of implementing the same property logic repeatedly, you can delegate that responsibility to helper objects.
In Kotlin, a delegated property allows a separate object (the delegate) to handle the property’s getter and setter logic. Instead of managing the property manually, you delegate its implementation. Kotlin provides built-in support for property delegation with the by keyword.
Understanding Delegated Properties
A delegated property is a property whose getter (and optionally setter) is outsourced to another object, called the delegate. The delegate handles the property’s storage and provides the logic for accessing and modifying its value.
The basic syntax looks like this:
Open Kotlin play ground and write the following program.
import kotlin.reflect.KProperty
class Example {
var property: String by Delegate()
}
class Delegate {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "Hi BigKnol : Provides this value!"
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("Hi BigKnol : setValue stores the value: $value")
}
}
fun main() {
val example = Example()
println(example.property)
example.property = "New Value" // Prints: setValue stores the value: New Value
}
Output
Hi BigKnol : Provides this value!
Hi BigKnol : setValue stores the value: New Value
The by keyword is what connects the property with its delegate.
Standard Delegates in Kotlin
Kotlin’s standard library includes several ready-to-use delegates:
Lazy Properties
The lazy delegate allows you to compute a value only when it’s first accessed:
class ExpensiveResource {
val data: String by lazy {
println("Computing expensive data...")
// Simulate expensive operation
Thread.sleep(1000)
"Expensive data result"
}
}
fun main() {
val resource = ExpensiveResource()
println("Resource created, but data not yet computed")
// First access triggers computation
println("First access: ${resource.data}")
// Subsequent accesses use cached value
println("Second access: ${resource.data}")
}
Output
Resource created, but data not yet computed
Computing expensive data...
First access: Expensive data result
Second access: Expensive data result
The lazy delegate is thread-safe by default, making it suitable for concurrent applications.
Observable Properties
The observable delegate lets you react to property changes:
import kotlin.properties.Delegates
class User {
var name: String by Delegates.observable("Initial") { property, oldValue, newValue ->
println("${property.name} changed from '$oldValue' to '$newValue'")
}
}
fun main() {
val user = User()
user.name = "Nikin"
user.name = "Hari"
}
Output
name changed from 'Initial' to 'Nikin'
name changed from 'Nikin' to 'Hari'
Vetoable Properties
The vetoable delegate lets you validate property changes before they happen:
import kotlin.properties.Delegates
class User {
var age: Int by Delegates.vetoable(0) { property, oldValue, newValue ->
if (newValue >= 0) {
println("Age changed from $oldValue to $newValue")
true // Accept the change
} else {
println("Rejecting negative age: $newValue")
false // Reject the change
}
}
}
fun main() {
val user = User()
user.age = 25
println("Current age: ${user.age}")
user.age = -5
println("Current age: ${user.age}")
user.age = 30
println("Current age: ${user.age}")
}
Output
Age changed from 0 to 25
Current age: 25
Rejecting negative age: -5
Current age: 25
Age changed from 25 to 30
Current age: 30
Map-Based Delegation
You can delegate properties to a map, which is helpful for dynamic objects:
class EnvironmentConfig(private val map: Map<String, Any?>) {
val apiKey: String by map
val timeout: Int by map
val baseUrl: String by map
}
fun main() {
val config = EnvironmentConfig(mapOf(
"apiKey" to "abc123xyz",
"timeout" to 30000,
"baseUrl" to "https://api.example.com"
))
println("API Key: ${config.apiKey}")
println("Timeout: ${config.timeout}")
println("Base URL: ${config.baseUrl}")
}
Output
API Key: abc123xyz
Timeout: 30000
Base URL: https://api.example.com
Creating Custom Property Delegates
Custom delegates give you complete control over property behavior. Let’s create a delegate that validates email addresses:
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class EmailValidator : ReadWriteProperty<Any?, String> {
private var email: String = ""
override fun getValue(thisRef: Any?, property: KProperty<*>): String {
return email
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
if (!value.matches(Regex("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,6}$"))) {
throw IllegalArgumentException("Invalid email format: $value")
}
email = value
}
}
class User {
var email: String by EmailValidator()
}
fun main() {
val user = User()
try {
user.email = "invalid-email"
} catch (e: IllegalArgumentException) {
println("Error: ${e.message}")
}
user.email = "user@example.com"
println("Valid email set: ${user.email}")
try {
user.email = "another@invalid"
} catch (e: IllegalArgumentException) {
println("Error: ${e.message}")
}
}
Output
Error: Invalid email format: invalid-email
Valid email set: user@example.com
Error: Invalid email format: another@invalid
Kotlin Advanced Use Case: Cached Properties
Let’s implement a cache system using delegated properties:
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
class CachedProperty<in R, out T>(private val compute: () -> T) : ReadOnlyProperty<R, T> {
private var value: T? = null
private var initialized = false
override fun getValue(thisRef: R, property: KProperty<*>): T {
if (!initialized) {
println("Computing value for ${property.name}...")
value = compute()
initialized = true
} else {
println("Returning cached value for ${property.name}")
}
return value as T
}
fun invalidate() {
initialized = false
value = null
}
}
class DataRepository {
val users: List<String> by CachedProperty {
// Simulate database query
println("Fetching users from database...")
Thread.sleep(1000)
listOf("Alice", "Bob", "Charlie")
}
val userCount: Int by CachedProperty {
println("Calculating user count...")
users.size // Uses the cached users list
}
fun clearCache() {
(this::users as? CachedProperty<*, *>)?.invalidate()
(this::userCount as? CachedProperty<*, *>)?.invalidate()
}
}
fun main() {
val repo = DataRepository()
// First access computes values
println("Users: ${repo.users}")
println("User count: ${repo.userCount}")
// Second access uses cached values
println("\nRetrieving again:")
println("Users: ${repo.users}")
println("User count: ${repo.userCount}")
// Clear cache and fetch again
println("\nClearing cache and retrieving again:")
repo.clearCache()
println("Users: ${repo.users}")
println("User count: ${repo.userCount}")
}
Output
Computing value for users...
Fetching users from database...
Users: [Alice, Bob, Charlie]
Computing value for userCount...
Calculating user count...
Returning cached value for users
User count: 3
Retrieving again:
Returning cached value for users
Users: [Alice, Bob, Charlie]
Returning cached value for userCount
User count: 3
Clearing cache and retrieving again:
Returning cached value for users
Users: [Alice, Bob, Charlie]
Returning cached value for userCount
User count: 3
Practical Use Cases for Delegated Properties
- Form validation: Create validators for different input types
- Configuration management: Load configurations dynamically
- Database access: Lazy-load database records only when needed
- Caching: Cache expensive computations until invalidated
- Dependency injection: Inject dependencies into properties
- Thread safety: Ensure safe property access in concurrent environments
- Binding UI elements: Connect UI components to data models
Performance Considerations
While delegated properties offer great flexibility, they come with a small overhead:
- Each property access involves extra function calls
- Reflection is used in some delegates (like observable and vetoable)
- Creating many delegate instances can increase memory usage
For most applications, this overhead is negligible, but in performance-critical sections, direct property access might be preferred.
Best Practices
- Use standard delegates when possible rather than creating custom ones
- Document your delegates clearly, especially custom ones
- Keep delegates focused on a single responsibility
- Consider thread safety when designing delegates for concurrent access
- Test edge cases thoroughly, as delegates can introduce subtle bugs
- Be cautious with recursive property access within delegates
Conclusion
Kotlin delegated properties offer a powerful way to extract and reuse common property behaviors. Whether you’re implementing lazy initialization, validation, or dynamic binding, delegates can help make your code more maintainable and concise.
Remember that like any powerful feature, delegates should be used thoughtfully. They’re most valuable when they help encapsulate complex property logic that would otherwise be duplicated across multiple classes.
What delegated property pattern will you implement in your next Kotlin project?
Happy Coding!