Level Up Your Kotlin: Unwrapping the Decorator Pattern with Modern Flair
Move beyond bloated constructors and messy flags with idiomatic functional decorators and extension functions.
Ever found yourself with a class whose constructor just keeps growing? Or maybe you’ve added features that required modifying stable, unit-tested code? You’ve likely encountered the “Ever-Expanding Constructor” syndrome — the perfect use case for the Decorator Pattern.
In this guide, we’ll move from a messy, flag-based architecture to the classic “Gang of Four” structure, and finally to a modern Kotlin implementation that feels like magic.
The Problem: Feature Creep & “The God Constructor”
Imagine you’re building a reporting tool. It starts simple, but soon the business asks for timestamps, unique tracking IDs, and user metadata.
// ⚠️ ANTI-PATTERN: The "God Constructor"
class ComplexReportGenerator(
private val includeTimestamp: Boolean = false,
private val includeUniqueId: Boolean = false,
private val includeUserName: Boolean = false,
private val clock: Clock = Clock.systemDefaultZone() // Dependency bloat: why is this here if flag is false?
) {
fun generate(data: String): String {
var report = "Report: $data"
if (includeTimestamp) report = "${LocalDateTime.now(clock)} - $report"
if (includeUniqueId) report = "[ID:${UUID.randomUUID().toString().take(8)}] $report"
return report
}
}Why this fails:
- Violates Open/Closed Principle: You must modify this class every time a new feature is requested.
- Dependency Pollution: The generator needs to know about
Clockeven if timestamps are disabled. - Fragile: Changing logic for one flag might accidentally break another in the same
generateblock.
1. The Traditional Way: Gang of Four (GoF)
The classic solution is to wrap the original object in a “Decorator” that implements the same interface.
interface ReportGenerator {
fun generate(data: String): String
}
class BasicReportGenerator : ReportGenerator {
override fun generate(data: String): String = "Report: $data"
}
// The Base Decorator delegates all calls to the wrapped object
abstract class ReportDecorator(protected val inner: ReportGenerator) : ReportGenerator {
override fun generate(data: String): String = inner.generate(data)
}
class TimestampDecorator(inner: ReportGenerator, private val clock: Clock) : ReportDecorator(inner) {
override fun generate(data: String): String = "${LocalDateTime.now(clock)} - ${super.generate(data)}"
}The Verdict: Structurally perfect, but leads to “Class Explosion.” You end up with a dozen files just to add a few string prefixes.
2. The Kotlin Way: Functional Decorators
Kotlin allows us to treat the Decorator Pattern as a form of Function Composition. By using fun interface and extension functions, we layer behavior from the outside without the hierarchy overhead.
// 1. The Component as a Functional Interface
fun interface ReportGenerator {
fun generate(data: String): String
}
// 2. Concrete Component
fun basicReportGenerator() = ReportGenerator { data -> "Report: $data" }
// 3. Decorators as Extension Functions
fun ReportGenerator.withTimestamp(clock: Clock): ReportGenerator = ReportGenerator { data ->
"${LocalDateTime.now(clock)} - ${this.generate(data)}"
}
fun ReportGenerator.withUniqueId(): ReportGenerator = ReportGenerator { data ->
"[ID:${UUID.randomUUID().toString().take(8)}] ${this.generate(data)}"
}Creating the Final Object (Fluent API)
val clock = Clock.systemDefaultZone()
val finalGenerator = basicReportGenerator()
.withUniqueId()
.withTimestamp(clock)
println(finalGenerator.generate("Sales Figures"))
// Output: 2023-10-26T10:46:00 - [ID:e5f6g7h8] Report: Sales FiguresPro Tip: Decorators apply in reverse order of invocation (like mathematical function composition). In the example above,
withTimestampwraps the result ofwithUniqueId.
Deep Dive: Senior Credibility
Is this “Real” Decorator or just Functional Trickery?
It’s a true Decorator. It wraps an object and enhances behavior while maintaining the interface. Kotlin simply uses Dependency Locality — passing the Clock only to the decorator that needs it, rather than the base generator.
Testing: Isolated and Predictable
Testing functional decorators is trivial because they are stateless transformations.
@Test
fun `adds timestamp correctly`() {
val fixedClock = Clock.fixed(Instant.parse("2025-12-17T10:00:00Z"), ZoneOffset.UTC)
val generator = basicReportGenerator().withTimestamp(fixedClock)
val result = generator.generate("Test Data")
assertEquals("2025-12-17T10:00:00 - Report: Test Data", result)
}Performance & Thread Safety
- JVM Reality: Each decorator creates a lightweight lambda. Thanks to JIT inlining, this is almost never a bottleneck outside of extreme high-frequency loops.
- Immutability: These decorators are pure transformations. They don’t maintain internal state, making them inherently thread-safe.
Frequently Asked Questions (FAQs)
Doesn’t this make debugging harder?
Stack traces will show lambdas, but because the logic is encapsulated in small extension functions, finding the source of a bug is usually faster than digging through a 500-line “God Class.”
What’s the difference between this and Strategy?
Strategy replaces internal logic. Decorator wraps the whole object to add or modify behavior transparently.
Is this just Middleware?
Yes! Middleware is simply the Decorator Pattern applied to the lifecycle of a request.
Challenges to Master the Pattern
- The Placement Challenge: How would you design your decorators so that the
Timestampalways appears at the start of the string, regardless of whether it's called first or last in the chain? - The Error Decorator: Create a
.withErrorHandling()decorator that catches exceptions in the generation process and returns a fallback message.
If you find my content useful, feel free to support my work here. Every contribution is greatly appreciated! 🙏

Comments
Post a Comment