Imagine your platform needs to export financial reports. Some reports are exported as plain text. Some need to be compressed because they are huge. Some contain sensitive data and must be encrypted. Some need an audit trail for compliance. Any given report might need one of these behaviors, all of them, or none at all - and that combination is often only known at runtime, based on user settings or report type.
If you try to solve this with subclassing, you quickly end up with CompressedReportExporter, EncryptedReportExporter, CompressedEncryptedReportExporter, AuditedCompressedEncryptedReportExporter, and so on. That is the classic subclass explosion the Decorator pattern exists to prevent: instead of baking every combination into the class hierarchy, you wrap a base object with small, focused decorators that can be composed in any order.
The Problem: One Behavior, Many Optional Add-ons
The base requirement is simple: export report content as a string. The complexity comes from the optional, independently toggleable behaviors layered on top:
- Compression for large reports.
- Encryption for reports containing sensitive data.
- Audit logging for compliance-sensitive exports.
None of these should require touching the others, and none should require the base exporter to know they exist.
Key Concepts
| Concept | In this example | Why it matters |
|---|---|---|
| Component interface | ReportExporter |
The stable contract every exporter (base or decorated) implements |
| Concrete component | PlainTextReportExporter |
The base object that does the real, minimal work |
| Base decorator | ReportExporterDecorator |
Holds the wrapped component and forwards calls to it |
| Concrete decorators | CompressionDecorator, EncryptionDecorator, AuditLoggingDecorator |
Each adds one independent behavior around the wrapped exporter |
| Client | ReportPublishingService |
Builds a decorator chain from feature flags, unaware of how many decorators are stacked |
Real Use Case: Publishing Reports With Configurable Compliance Rules
Picture a reporting platform used by finance and operations teams. Every export request carries a few independent requirements:
- Large reports (quarterly filings, transaction logs) should be compressed before they leave the service.
- Reports containing customer or financial data must be encrypted at rest and in transit.
- Regulated exports must be logged for audit purposes - who exported what, and how much data left the system.
Crucially, these requirements are orthogonal: a small internal report might need none of them, while a large, sensitive, regulated report needs all three at once. The Decorator pattern lets ReportPublishingService build exactly the right chain for each request, at runtime, using simple boolean flags - without ever creating a subclass like CompressedEncryptedAuditedReportExporter.
Component Walkthrough: What Each Part Is Doing
ReportExporter(Component Interface) defines the single operation every exporter must support:exportReport(content). Both the base exporter and every decorator implement this same interface, which is what allows them to be swapped and stacked transparently.PlainTextReportExporter(Concrete Component) is the simplest possible implementation: it returns the content unchanged. This is the object every decorator chain eventually wraps.ReportExporterDecorator(Base Decorator) is an abstract class that stores a reference to the wrappedReportExporter(thedelegate). It exists purely to avoid repeating that plumbing in every concrete decorator.CompressionDecoratorcalls the delegate first, then wraps the result with size metadata - demonstrating a decorator that transforms output after delegating.EncryptionDecoratoralso transforms the output (via a reversible XOR cipher encoded as Base64) and additionally exposes adecrypthelper, showing that a decorator can carry extra capabilities beyond the shared interface.AuditLoggingDecoratorcalls the delegate and returns its result unchanged, but records a side-effect (an audit entry) - demonstrating that decorators do not have to transform data to be useful.ReportPublishingService(Client) builds the decorator chain dynamically fromcompress,encrypt, andauditflags, then callsexportReportonce on the fully assembled chain.
Request Flow: Stacking Decorators at Runtime
For a report that needs compression, encryption, and an audit trail, the chain is assembled like this:
- Start with
PlainTextReportExporter(the concrete component). - Wrap it in
CompressionDecoratorif the report is large. - Wrap that in
EncryptionDecoratorif the report is sensitive. - Wrap that in
AuditLoggingDecoratorif the export must be logged. - Call
exportReport(content)once on the outermost decorator - each layer delegates inward, then applies its own behavior on the way back out.
Because every layer honors the same ReportExporter contract, the order of decorators can change the result (compress-then-encrypt differs from encrypt-then-compress), which is a deliberate part of the pattern: composition order is a design decision, not an accident.
The Solution: Decorator Across JVM Languages
Below is the target interface, the base decorator, and one concrete decorator (CompressionDecorator) in Java 21, Kotlin, Scala 2, and Scala 3. The full source for all three decorators is linked at the end of this post.
public sealed interface ReportExporter permits PlainTextReportExporter, ReportExporterDecorator {
String exportReport(String content);
}
abstract sealed class ReportExporterDecorator implements ReportExporter
permits CompressionDecorator, EncryptionDecorator, AuditLoggingDecorator {
protected final ReportExporter delegate;
protected ReportExporterDecorator(ReportExporter delegate) {
this.delegate = delegate;
}
}
final class CompressionDecorator extends ReportExporterDecorator {
public CompressionDecorator(ReportExporter delegate) {
super(delegate);
}
@Override
public String exportReport(String content) {
String exported = delegate.exportReport(content);
return "COMPRESSED[" + exported.length() + "]:" + exported;
}
}
sealed interface ReportExporter {
fun exportReport(content: String): String
}
sealed class ReportExporterDecorator(
protected val delegate: ReportExporter,
) : ReportExporter
class CompressionDecorator(
delegate: ReportExporter,
) : ReportExporterDecorator(delegate) {
override fun exportReport(content: String): String {
val exported = delegate.exportReport(content)
return "COMPRESSED[${exported.length}]:$exported"
}
}
trait ReportExporter {
def exportReport(content: String): String
}
abstract class ReportExporterDecorator(protected val delegate: ReportExporter)
extends ReportExporter
class CompressionDecorator(delegate: ReportExporter) extends ReportExporterDecorator(delegate) {
override def exportReport(content: String): String = {
val exported = delegate.exportReport(content)
s"COMPRESSED[${exported.length}]:$exported"
}
}
trait ReportExporter:
def exportReport(content: String): String
abstract class ReportExporterDecorator(protected val delegate: ReportExporter)
extends ReportExporter
class CompressionDecorator(delegate: ReportExporter) extends ReportExporterDecorator(delegate):
override def exportReport(content: String): String =
val exported = delegate.exportReport(content)
s"COMPRESSED[${exported.length}]:$exported"
Scala Developer Mental Model
- In Java 21,
sealedinterfaces and classes withpermitsmake the decorator hierarchy exhaustive and explicit: the compiler knows exactly which classes can implementReportExporteror extendReportExporterDecorator. - In Scala 2/3, traits and abstract classes give you the same shape with less ceremony. Because there is no built-in equivalent of
permits, the hierarchy is closed by convention rather than by the compiler (unless you addsealed trait). - In Kotlin,
sealed interfaceplus asealed classbase decorator mirrors Java’s exhaustiveness guarantees while keeping constructor boilerplate minimal thanks to primary constructor properties.
Comparison: Java 21 vs Scala 2 vs Scala 3 vs Kotlin
| Language | Component contract | Base decorator | Composability |
|---|---|---|---|
| Java 21 | sealed interface + permits |
abstract sealed class |
Explicit constructor chaining via super(delegate) |
| Scala 2 | trait |
abstract class with a protected val |
Constructor chaining via extends ReportExporterDecorator(delegate) |
| Scala 3 | trait (colon syntax) |
abstract class with a protected val |
Same as Scala 2, less boilerplate |
| Kotlin | sealed interface |
sealed class with primary constructor property |
Very concise; sealed is implicitly abstract |
Testing the Decorator: Proving Stackability
The most important test for a decorator is not any single decorator in isolation - it is proving that decorators compose: the final result reflects every layer, in the order they were applied, and behaviors like audit logging can be verified independently of the data transformation.
@Test
void shouldStackMultipleDecoratorsInAnyOrder() {
List<String> auditLog = new ArrayList<>();
ReportExporter exporter = new AuditLoggingDecorator(
new EncryptionDecorator(new CompressionDecorator(new PlainTextReportExporter()), 7),
auditLog);
String exported = exporter.exportReport(REPORT);
assertTrue(exported.startsWith("ENCRYPTED:"));
assertEquals(1, auditLog.size());
}
@Test
fun shouldStackMultipleDecoratorsInAnyOrder() {
val auditLog = mutableListOf<String>()
val exporter: ReportExporter =
AuditLoggingDecorator(
EncryptionDecorator(CompressionDecorator(PlainTextReportExporter()), 7),
auditLog,
)
val exported = exporter.exportReport(report)
assertTrue(exported.startsWith("ENCRYPTED:"))
assertEquals(1, auditLog.size)
}
@Test
@DisplayName("Decorators should stack in any order without a subclass explosion")
def shouldStackMultipleDecoratorsInAnyOrder(): Unit = {
val auditLog = ArrayBuffer.empty[String]
val exporter: ReportExporter = new AuditLoggingDecorator(
new EncryptionDecorator(new CompressionDecorator(new PlainTextReportExporter), 7),
auditLog
)
val exported = exporter.exportReport(report)
assertTrue(exported.startsWith("ENCRYPTED:"))
assertEquals(1, auditLog.size)
}
@Test
@DisplayName("Decorators should stack in any order without a subclass explosion")
def shouldStackMultipleDecoratorsInAnyOrder(): Unit =
val auditLog = ArrayBuffer.empty[String]
val exporter: ReportExporter = AuditLoggingDecorator(
EncryptionDecorator(CompressionDecorator(PlainTextReportExporter()), 7),
auditLog
)
val exported = exporter.exportReport(report)
assertTrue(exported.startsWith("ENCRYPTED:"))
assertEquals(1, auditLog.size)
Each language’s test suite also verifies the simpler building blocks - a plain export leaves content untouched, compression adds size metadata, encryption is reversible with the correct key, and audit logging records an entry without altering the exported content - plus a realistic scenario where ReportPublishingService assembles the whole chain from compress / encrypt / audit flags, exactly like a production feature-flag-driven export pipeline would.
When to Use the Decorator Pattern
Use a decorator when:
- You need to add optional, independently combinable behaviors to an object.
- The number of possible combinations would otherwise force a subclass per combination.
- You want to add or remove a behavior without touching the component’s core logic.
- Behaviors should be composable and reorderable at runtime, not fixed at compile time.
Avoid it when:
- There is only one fixed combination of behaviors - a single class is simpler.
- The “behaviors” actually change the object’s core identity or contract, not just add to it (that is closer to Strategy or plain inheritance).
- Deep decorator chains become hard to debug; consider a pipeline or middleware abstraction instead once the chain grows very long.
Code Samples
All examples in this post are available in the repository:
Implementation files:
- Java 21: ReportExporter.java
- Kotlin: ReportExporter.kt
- Scala 2: ReportExporter.scala
- Scala 3: ReportExporter.scala
Test files:
- Java 21: ReportExporterTest.java
- Kotlin: ReportExporterTest.kt
- Scala 2: ReportExporterTest.scala
- Scala 3: ReportExporterTest.scala
This is part of our Design Patterns in JVM Languages series. Check out the full design patterns guide for more patterns and interview preparation.