Design Patterns in Java

Decorator Pattern

15 min Lesson 7 of 13

Decorator Pattern

The Decorator pattern lets you attach new behaviors to an object at runtime by wrapping it inside another object that shares the same interface. Instead of creating a combinatorial explosion of subclasses, you compose small, focused decorators in any order you need.

Why Not Just Subclass?

Suppose you have a TextEditor and want to support spell-checking, auto-save, and syntax highlighting — in any combination. Subclassing every combination gives you seven classes for three features. Add a fourth feature and you double that number again. Decorator sidesteps this entirely: each feature lives in one class and wraps any other TextEditor.

Open/Closed Principle in action: Decorator is the textbook implementation of the Open/Closed Principle — components are open for extension (wrap them) but closed for modification (never touch them).

Structure

  • Component — the interface (or abstract class) shared by the real object and every decorator.
  • ConcreteComponent — the base implementation you want to extend.
  • BaseDecorator — holds a reference to a Component and delegates all calls; subclasses override only the methods they need to augment.
  • ConcreteDecorators — add the actual behavior before and/or after delegating.

A Text-Transformation Example

Start with a simple component interface and a plain implementation:

// Component public interface TextTransformer { String transform(String input); } // ConcreteComponent public class PlainText implements TextTransformer { @Override public String transform(String input) { return input; } }

The base decorator stores the wrapped component and forwards every call to it:

public abstract class TextDecorator implements TextTransformer { private final TextTransformer wrapped; protected TextDecorator(TextTransformer wrapped) { this.wrapped = wrapped; } @Override public String transform(String input) { return wrapped.transform(input); // pure delegation by default } }

Concrete decorators add behavior around that delegation:

// Trims whitespace before delegating public class TrimDecorator extends TextDecorator { public TrimDecorator(TextTransformer wrapped) { super(wrapped); } @Override public String transform(String input) { return super.transform(input.strip()); } } // Converts the result to upper-case after delegating public class UpperCaseDecorator extends TextDecorator { public UpperCaseDecorator(TextTransformer wrapped) { super(wrapped); } @Override public String transform(String input) { return super.transform(input).toUpperCase(); } } // Replaces every tab with four spaces public class TabExpandDecorator extends TextDecorator { public TabExpandDecorator(TextTransformer wrapped) { super(wrapped); } @Override public String transform(String input) { return super.transform(input.replace("\t", " ")); } }

Compose them in any order at the call site:

TextTransformer pipeline = new UpperCaseDecorator( new TrimDecorator( new TabExpandDecorator( new PlainText() ) ) ); String result = pipeline.transform(" \thello world "); // → " HELLO WORLD" (tab expanded, then trimmed, then uppercased)
Read the nesting inside-out: the innermost constructor runs first. In the example above the order of execution is TabExpand → Trim → UpperCase. Choose the wrapping order that reads most naturally for your domain.

java.io — The Classic Real-World Decorator Stack

The entire java.io stream hierarchy is built on Decorator. InputStream is the Component; FileInputStream, ByteArrayInputStream, etc. are ConcreteComponents; and FilterInputStream is the BaseDecorator. Every class you layer on top — BufferedInputStream, DataInputStream, GZIPInputStream — is a ConcreteDecorator.

import java.io.*; import java.util.zip.GZIPInputStream; // Read a gzipped file, buffered, line-by-line try (var reader = new BufferedReader( new InputStreamReader( new GZIPInputStream( new FileInputStream("data.gz") ), java.nio.charset.StandardCharsets.UTF_8 ) )) { String line; while ((line = reader.readLine()) != null) { System.out.println(line); } }

Each wrapper adds exactly one capability — buffering, charset decoding, gzip decompression, file reading — without any of the classes needing to know about each other. You can swap FileInputStream for ByteArrayInputStream (for tests) without touching any other layer.

Trade-offs and When to Use It

  • Prefer it over inheritance when features are genuinely orthogonal and combinable.
  • Prefer it over a single fat class that switches behavior with flags — decorators make each concern testable in isolation.
  • Watch out for order sensitivity: trimming then uppercasing is not the same as uppercasing then trimming when locale-specific casing is involved.
  • Equality and identity break: decorated.equals(original) is almost always false. Do not rely on object identity when decorators are in play.
  • Deep stacks are hard to debug: a chain of ten decorators can obscure the source of an unexpected result. Keep each decorator small and document the expected order.
Decorator vs Proxy: both wrap an object, but with different intent. A Proxy controls access (lazy load, caching, security check). A Decorator adds behavior. If you find yourself checking permissions or lazily initializing inside a "decorator", you probably want a Proxy instead.

Decorator with Java Functional Interfaces (Modern Alternative)

For simple single-method transformations, Function::andThen and Function::compose give you the same composition without any class hierarchy:

import java.util.function.UnaryOperator; UnaryOperator<String> trim = String::strip; UnaryOperator<String> upper = String::toUpperCase; UnaryOperator<String> pipeline = trim.andThen(upper); String result = pipeline.apply(" hello "); // "HELLO"

The class-based Decorator pattern remains the right tool when the component interface has multiple methods, when decorators carry state (e.g. a counting decorator), or when you need to pass the decorator through code that expects the component type.

Summary

Decorator wraps an object in another object that shares its interface, adding behavior without touching the original. java.io streams are its most famous application. Keep decorators stateless where possible, document composition order, and prefer the functional style for single-operation pipelines. The pattern shines whenever you have a set of independently useful behaviors that must combine freely at runtime.