Kotlin Delegation and Property Delegation Explained
Delegation is Kotlin's killer language feature — one line for thread-safe lazy initialization, one line for property change monitoring. Charlie uses class delegation to replace Decorator pattern boilerplate, and property delegation to auto-persist OrderProcessor configuration properties.
1. What You'll Learn
- Class delegation: Decorator pattern with zero boilerplate
by lazy: thread-safe lazy computingDelegates.observable/Delegates.vetoable: property change monitoring and interception- Custom property delegation:
ReadWriteProperty/getValue/setValue - Charlie in action:
ValidatedRepository+ConfigPropertycustom delegation
2. A Real Architect's Story
(1) Pain Point: Decorator Pattern Boilerplate
Charlie used the Java Decorator pattern to add logging and caching to OrderRepository. Every layer required 20+ lines of delegation methods — the interface has 8 methods, and 3 decorator layers meant 480 lines of pure delegation code.
(2) Kotlin's Class Delegation Solution
KOTLIN
// Java: 20+ lines per method per decorator
class LoggedRepository implements OrderRepository {
private final OrderRepository delegate;
@Override public Order findById(String id) { log(); return delegate.findById(id); }
// ... repeat for all 8 methods
}
// Kotlin: 1 line delegates ALL methods
class LoggedRepository(
private val delegate: OrderRepository
) : OrderRepository by delegate // Auto-delegates all methods!
One line replaces 20+ lines of method delegation — only override the methods you want to enhance.
3. Class Delegation (by)
(1) Basic Syntax
KOTLIN
interface OrderRepository {
fun findById(id: String): Order?
fun save(order: Order)
fun findAll(): List<Order>
}
class InMemoryRepository : OrderRepository {
private val storage = mutableMapOf<String, Order>()
override fun findById(id: String) = storage[id]
override fun save(order: Order) { storage[order.id] = order }
override fun findAll() = storage.values.toList()
}
// Class delegation: auto-delegates all interface methods
class LoggedRepository(
private val inner: OrderRepository
) : OrderRepository by inner { // All methods delegated!
// Override only what you want to enhance
override fun save(order: Order) {
println("Saving order: ${order.id}")
inner.save(order) // Call delegate explicitly
}
}
(2) Multi-Layer Delegation
KOTLIN
// Layer 1: Caching
class CachedRepository(
private val delegate: OrderRepository
) : OrderRepository by delegate {
private val cache = mutableMapOf<String, Order>()
override fun findById(id: String): Order? {
return cache.getOrPut(id) { delegate.findById(id)!! }
}
}
// Layer 2: Logging
class LoggedRepository(
private val delegate: OrderRepository
) : OrderRepository by delegate {
override fun save(order: Order) {
println("Saving: ${order.id}")
delegate.save(order)
}
}
// Compose: logging + caching + in-memory
val repo = LoggedRepository(CachedRepository(InMemoryRepository()))
(3) Class Delegation vs Inheritance vs Decorator
| Dimension | Inheritance | Decorator (Java) | Class Delegation (Kotlin) |
|---|---|---|---|
| Code volume | Low | High (boilerplate) | Low (by) |
| Flexibility | Single inheritance | Multi-layer composition | Multi-layer composition |
| Coupling | High | Low | Low |
| Runtime | Parent class binding | Delegate object binding | Delegate object binding |
4. Property Delegation: by lazy
(1) Basic Usage
KOTLIN
class OrderProcessor {
// Lazy: computed on first access, thread-safe by default
val cache: OrderCache by lazy {
println("Initializing cache...")
OrderCache(maxSize = 10_000)
}
// Lazy with custom thread safety
val heavyConfig by lazy(LazyThreadSafetyMode.PUBLICATION) {
loadConfig() // May be called multiple times, but only first result used
}
}
(2) lazy Thread Safety Modes
| Mode | Behavior | Performance | Use Case |
|---|---|---|---|
SYNCHRONIZED (default) |
Single init, double-checked locking | Slightly slower | Multi-threaded environments |
PUBLICATION |
Multiple threads may compute, only first result used | Faster | Init with no side effects |
NONE |
No synchronization | Fastest | Known single-threaded context |
5. Property Delegation: observable / vetoable
(1) observable — Change Notification
KOTLIN
import kotlin.properties.Delegates
class Order(var id: String) {
// observable: notify on every change
var status: String by Delegates.observable("PENDING") { _, old, new ->
println("Status changed: $old -> $new")
}
}
val order = Order("ORD-001")
order.status = "CONFIRMED" // Prints: Status changed: PENDING -> CONFIRMED
order.status = "SHIPPED" // Prints: Status changed: CONFIRMED -> SHIPPED
(2) vetoable — Change Interception
KOTLIN
class Order(var id: String) {
// vetoable: can reject the change
var total: Double by Delegates.vetoable(0.0) { _, old, new ->
if (new < 0) {
println("Rejecting negative total: $new")
false // Veto: keep old value
} else {
true // Accept the change
}
}
}
val order = Order("ORD-001")
order.total = 299.99 // Accepted
order.total = -50.0 // Rejected: prints warning, keeps 299.99
(3) observable vs vetoable Comparison
| Delegate | Behavior | Return Value | Use Case |
|---|---|---|---|
observable |
Notify change, don't intercept | Unit | Logging / auditing |
vetoable |
Can intercept and reject change | Boolean | Validation / constraints |
6. Custom Property Delegation
(1) ReadWriteProperty Interface
KOTLIN
import kotlin.reflect.KProperty
class PersistentProperty<T>(
private val key: String,
private val defaultValue: T
) : ReadWriteProperty<Any?, T> {
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
// Load from persistence (simplified)
return loadFromStore(key) ?: defaultValue
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
// Save to persistence
saveToStore(key, value)
}
}
// Simulated persistence
private val store = mutableMapOf<String, Any?>()
private fun <T> loadFromStore(key: String): T? = store[key] as T?
private fun <T> saveToStore(key: String, value: T) { store[key] = value }
(2) Using Custom Delegates
KOTLIN
class OrderConfig {
var maxRetries: Int by PersistentProperty("order.max_retries", 3)
var timeout: Long by PersistentProperty("order.timeout", 5_000L)
var taxRate: Double by PersistentProperty("order.tax_rate", 0.08)
}
val config = OrderConfig()
println(config.maxRetries) // 3 (default)
config.maxRetries = 5
println(config.maxRetries) // 5 (persisted)
(3) Delegate Class Diagram
classDiagram
class ReadOnlyProperty {
<<interface>>
+getValue(thisRef, property): T
}
class ReadWriteProperty {
<<interface>>
+getValue(thisRef, property): T
+setValue(thisRef, property, value: T)
}
class LazyDelegate {
+getValue(): T
}
class ObservableDelegate {
+setValue()
}
class VetoableDelegate {
+setValue()
}
class CustomDelegate {
+getValue()
+setValue()
}
ReadOnlyProperty <|-- ReadWriteProperty
ReadOnlyProperty <|-- LazyDelegate
ReadWriteProperty <|-- ObservableDelegate
ReadWriteProperty <|-- VetoableDelegate
ReadWriteProperty <|-- CustomDelegate
7. Complete Example: OrderProcessor Delegation Architecture
KOTLIN
// ============================================
// OrderProcessor - Delegation Architecture
// Feature: Class delegation + property delegation
// ============================================
import kotlin.properties.Delegates
import kotlin.reflect.KProperty
// --- Class Delegation ---
interface OrderRepository {
fun findById(id: String): Order?
fun save(order: Order)
fun findAll(): List<Order>
}
data class Order(val id: String, var total: Double, var status: String, val customer: String)
class InMemoryRepository : OrderRepository {
private val storage = mutableMapOf<String, Order>()
override fun findById(id: String) = storage[id]
override fun save(order: Order) { storage[order.id] = order }
override fun findAll() = storage.values.toList()
}
class LoggedRepository(private val inner: OrderRepository) : OrderRepository by inner {
override fun save(order: Order) {
println(" [LOG] Saving order: ${order.id} (\$${order.total} USD)")
inner.save(order)
}
}
class ValidatedRepository(private val inner: OrderRepository) : OrderRepository by inner {
override fun save(order: Order) {
require(order.total >= 0) { "Total must be non-negative" }
require(order.id.startsWith("ORD-")) { "Invalid order ID format" }
inner.save(order)
}
}
// --- Property Delegation ---
private val configStore = mutableMapOf<String, Any?>()
class ConfigProperty<T>(private val key: String, private val default: T) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
@Suppress("UNCHECKED_CAST")
return configStore[key] as? T ?: default
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
configStore[key] = value
println(" [CONFIG] $key = $value")
}
}
class OrderProcessorConfig {
var maxOrderValue: Double by ConfigProperty("max_order_value", 100_000.0)
var defaultTaxRate: Double by ConfigProperty("default_tax_rate", 0.08)
var enableAuditing: Boolean by Delegates.observable(true) { _, _, new ->
println(" [CONFIG] Auditing ${if (new) "enabled" else "disabled"}")
}
var processingTimeout: Long by Delegates.vetoable(30_000L) { _, _, new ->
if (new < 1_000) { println(" [CONFIG] Rejecting timeout < 1000ms"); false }
else true
}
}
fun main() {
// Compose: validation + logging + in-memory
val repo: OrderRepository = ValidatedRepository(LoggedRepository(InMemoryRepository()))
println("=== Saving Orders ===")
repo.save(Order("ORD-001", 299.99, "CONFIRMED", "Alice"))
repo.save(Order("ORD-002", 15_000.00, "PENDING", "Bob"))
println("\n=== Find Order ===")
println(repo.findById("ORD-001"))
// Property delegation
println("\n=== Configuration ===")
val config = OrderProcessorConfig()
println("Max order value: \$${config.maxOrderValue} USD")
config.defaultTaxRate = 0.10
config.enableAuditing = false
config.processingTimeout = 500 // Rejected
config.processingTimeout = 60_000 // Accepted
println("Timeout: ${config.processingTimeout}ms")
}
Output:
TEXT
=== Saving Orders ===
[LOG] Saving order: ORD-001 ($299.99 USD)
[LOG] Saving order: ORD-002 ($15000.0 USD)
=== Find Order ===
Order(id=ORD-001, total=299.99, status=CONFIRMED, customer=Alice)
=== Configuration ===
Max order value: $100000.0 USD
[CONFIG] default_tax_rate = 0.1
[CONFIG] Auditing disabled
[CONFIG] Rejecting timeout < 1000ms
[CONFIG] processingTimeout = 60000
Timeout: 60000ms
❓ FAQ
Q What's the difference between class delegation and inheritance?
A Class delegation is composition (has-a); inheritance is subtyping (is-a). Delegation is more flexible — you can replace the delegate at runtime and avoid the tight coupling of inheritance.
Q Does
by lazy initialization always happen?A Not necessarily. If the property is never accessed, initialization never executes. This is
lazy's core advantage — avoiding unnecessary initialization overhead.Q What's the performance of property delegation?
A Each property read/write incurs an extra function call (getValue/setValue), but the JVM's inlining optimization typically eliminates this overhead. For hot paths, measure before deciding.
Q Can a class use multiple
by delegation?A Class delegation delegates one interface (multiple inheritance via multiple interfaces), and property delegation delegates each property independently.
Q Can
observable be used for reactive programming?A Its scope is limited — it only notifies within the same object. For cross-component reactivity, use StateFlow/SharedFlow.
observable is better suited for audit logs and simple notifications.Q Must custom delegates implement
ReadWriteProperty?A Not required. You only need to provide
getValue (and setValue for var). ReadWriteProperty is just a convenience interface.📖 Summary
- Class delegation with
byauto-delegates all interface methods — zero Decorator pattern boilerplate by lazyprovides thread-safe lazy initialization, ideal for on-demand creation of heavy objectsDelegates.observablemonitors property changes;Delegates.vetoablecan intercept and reject them- Custom property delegates only need
getValue/setValueoperator functions - Prefer delegation over inheritance — composition is more flexible than subclassing
- Multi-layer delegation composition achieves separation of concerns: logging + caching + validation
📝 Exercises
- Beginner (⭐): Use
by lazyto implement a lazy-initialized database connection that prints "Connecting..." on first access. Hint:val connection by lazy { ... } - Intermediate (⭐⭐): Use class delegation to implement
LoggedOrderRepo, overriding only thesavemethod to add logging. Hint:class LoggedOrderRepo(val inner: OrderRepository) : OrderRepository by inner { ... } - Challenge (⭐⭐⭐): Implement a custom
TraceDelegateproperty delegate that prints the property name and value on every read and write. Hint:getValue+setValueoperator functions



