Definition
- Decorator pattern – attaches additional responsibilities to an object dynamically by wrapping it in another object that shares the same interface.
- Component – the common type,
InputStream, that both real sources and decorators implement. - Concrete component – a real source like
FileInputStreamorByteArrayInputStream. - Abstract decorator –
FilterInputStream, which extendsInputStreamand holds a reference to a wrappedInputStream. - Concrete decorators –
BufferedInputStream,DataInputStream,GZIPInputStream, each adding exactly one capability. - Subclass explosion – without decorators, every combination of features requires a separate subclass, growing combinatorially.
Imagine National Bank of India sends a daily statement file. The raw file is plain data. Now you want it buffered for speed, compressed to save space, and read as structured numbers. Instead of inventing a special buffered-compressed-structured machine, you take the plain file and wrap it: first through a buffering layer, then through a decompression layer, then through a structured-reading layer. Each layer is a clip-on attachment that adds one capability and passes the data along. That snap-on-layers idea is the Decorator pattern.
Understanding the Concepts
What is the Decorator pattern in Java I/O?
The Decorator pattern has four roles in Java I/O. The component is InputStream. The concrete component is FileInputStream. The abstract decorator is FilterInputStream, which extends InputStream and holds a protected volatile InputStream in. The concrete decorators are BufferedInputStream, DataInputStream, and GZIPInputStream, each adding one capability.
package com.bank.io;
import java.io.*;
import java.util.zip.GZIPInputStream;
public class StatementReader {
public static void main(String[] args) throws Exception {
// Stack decorators: file → decompress → buffer → structured read
try (DataInputStream in = new DataInputStream(
new BufferedInputStream(
new GZIPInputStream(
new FileInputStream("statement.gz"))))) {
String accountId = in.readUTF();
double balance = in.readDouble();
System.out.println("Account: " + accountId
+ ", Balance: " + balance);
}
}
}
Why does the is-a and has-a relationship matter?
Each decorator is-a InputStream (because it extends InputStream) and has-a InputStream (because it holds a reference to the wrapped stream). Because it extends InputStream, it can be used anywhere an InputStream is expected, including as the thing another decorator wraps. Because it holds an inner stream, it can delegate the real work and add its own behaviour.
// is-a: DataInputStream IS an InputStream
InputStream stream = new DataInputStream(new FileInputStream("data.bin"));
// has-a: DataInputStream HAS an InputStream (the FileInputStream)
// This dual relationship enables unlimited stacking
How does each decorator add exactly one concern?
FileInputStream reads raw bytes from disk. GZIPInputStream transparently decompresses. BufferedInputStream reads a big chunk into a buffer so each small read does not hit the disk. DataInputStream adds methods to read primitive types.
package com.bank.io;
import java.io.*;
import java.math.BigDecimal;
public class BalanceFileWriter {
public static void main(String[] args) throws Exception {
// Write structured banking data
try (DataOutputStream out = new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream("balances.dat")))) {
out.writeUTF("SAV2001");
out.writeDouble(150000.75);
out.writeUTF("CUR3002");
out.writeDouble(89500.00);
}
// Read it back with the mirror stack
try (DataInputStream in = new DataInputStream(
new BufferedInputStream(
new FileInputStream("balances.dat")))) {
System.out.println(in.readUTF() + ": " + in.readDouble());
System.out.println(in.readUTF() + ": " + in.readDouble());
}
}
}
SAV2001: 150000.75
CUR3002: 89500.0
How does the Decorator pattern avoid subclass explosion?
Without decorators, combining features with inheritance would require a subclass for every combination:
FileInputStreamBufferedFileInputStreamGzipFileInputStreamDataFileInputStreamBufferedGzipFileInputStreamBufferedDataFileInputStreamGzipDataFileInputStreamBufferedGzipDataFileInputStream
With n features the number of subclasses grows as 2^n. The Decorator pattern collapses that: one decorator class per feature, composed at runtime. Three decorator classes give all eight combinations, and adding a fourth feature is one more class, not a doubling of the hierarchy.
// Without decorators: 8+ classes for 3 features (bad)
class BufferedGzipDataFileInputStream extends InputStream { ... }
// With decorators: 3 classes, composed freely (good)
new DataInputStream(new BufferedInputStream(new GZIPInputStream(
new FileInputStream("file.gz"))))
Why does wrapping order matter?
Buffering a compressed stream means you buffer the compressed bytes, reducing disk reads. Compressing a buffered stream means you buffer raw bytes and then compress them. The data flow is different, and performance characteristics change.
// Order 1: buffer after decompression (buffer decompressed bytes)
new BufferedInputStream(new GZIPInputStream(new FileInputStream("f.gz")))
// Order 2: buffer before decompression (buffer compressed bytes)
new GZIPInputStream(new BufferedInputStream(new FileInputStream("f.gz")))
// Order 2 is usually better: fewer disk reads of compressed data
How does closing work in a decorator chain?
Closing the outermost stream closes the whole chain. Each decorator's close() method calls close() on its inner stream, which calls close() on its inner stream, and so on down to the source. You only need one try-with-resources on the outer stream.
// One try-with-resources closes the entire chain
try (DataInputStream in = new DataInputStream(
new BufferedInputStream(
new FileInputStream("statement.dat")))) {
// read data
} // close() cascades: DataInputStream → BufferedInputStream → FileInputStream
How do you apply the Decorator pattern in banking code?
In a banking notification system, Rajesh can stack logging, encryption, and SMS sending as decorators:
package com.bank.decorator;
public interface Notifier {
void send(String message);
}
public class SmsNotifier implements Notifier {
@Override
public void send(String message) {
System.out.println("SMS to Priya: " + message);
}
}
public class LoggingDecorator implements Notifier {
private final Notifier inner;
public LoggingDecorator(Notifier inner) { this.inner = inner; }
@Override
public void send(String message) {
System.out.println("AUDIT LOG: " + message);
inner.send(message);
}
}
public class EncryptionDecorator implements Notifier {
private final Notifier inner;
public EncryptionDecorator(Notifier inner) { this.inner = inner; }
@Override
public void send(String message) {
inner.send("ENC[" + message + "]");
}
}
Notifier notifier = new LoggingDecorator(
new EncryptionDecorator(new SmsNotifier()));
notifier.send("Transfer of INR 50000 completed");
AUDIT LOG: Transfer of INR 50000 completed
SMS to Priya: ENC[Transfer of INR 50000 completed]
What are the limitations?
The layered design is powerful but the class hierarchy is large and sometimes confusing for newcomers. Each layer adds a small indirection overhead. And debugging a deeply wrapped chain requires understanding which layer is responsible for what behaviour.
What should a senior engineer know?
The same Decorator pattern appears on the output side with DataOutputStream, BufferedOutputStream, and GZIPOutputStream, and in the character streams with BufferedReader wrapping InputStreamReader. Recognising this in Java I/O immediately tells an interviewer you understand both the library and the pattern.
At National Bank of India, Pankaj uses exactly this approach to read and write compressed, buffered, structured balance-snapshot files, stacking DataOutputStream over BufferedOutputStream over GZIPOutputStream to write, and the mirror stack to read.
// Senior: recognise the pattern across all Java I/O
// Input side
new BufferedReader(new InputStreamReader(System.in))
// Output side
new PrintWriter(new BufferedWriter(new FileWriter("log.txt")))
// Both are Decorator chains
Visual Overview
Reading through a Decorator chain
─────────────────────────────────
DataInputStream ←── reads typed values (readInt, readUTF)
│
▼
BufferedInputStream ←── buffers for performance
│
▼
GZIPInputStream ←── decompresses
│
▼
FileInputStream ←── reads raw bytes from disk
│
▼
[ file.gz ]
Subclass explosion without Decorator
────────────────────────────────────
3 features → 2^3 = 8 subclasses
With Decorator:
3 decorator classes → all 8 combinations at runtime
FileInputStream ──→ GZIPInputStream ──→ BufferedInputStream
│ │
└────── any subset, any order ───────────┘
Key Takeaway
The Decorator pattern attaches extra responsibilities to an object dynamically by wrapping it in another object that shares the same interface, and Java's I/O stream library is the textbook example. On the input side the common type is InputStream. FileInputStream is the concrete source that reads raw bytes, and decorators like GZIPInputStream, BufferedInputStream, and DataInputStream each wrap another stream, add exactly one capability, and still expose the InputStream interface.
The structural trick is that each decorator is-an InputStream and also has-an InputStream. Because it extends InputStream it can be used anywhere a stream is expected, including as the thing another decorator wraps. Because it holds an inner stream it delegates the real work and adds its own behaviour. That dual relationship lets you compose a DataInputStream wrapping a BufferedInputStream wrapping a GZIPInputStream wrapping a FileInputStream, and reading from the outer stream pulls bytes through the whole chain transparently.
The reason this matters is subclass explosion. Without decorators, combining features with inheritance would require a separate subclass for every combination and ordering, which grows as 2^n. The Decorator pattern collapses that: one class per feature, composed at runtime in whatever order you need. Three decorator classes give every combination, and adding a fourth feature is one more class rather than doubling the hierarchy.
In the banking project at National Bank of India, Rajesh uses exactly this approach to read and write compressed, buffered, structured balance-snapshot files. The two nuances to mention are that wrapping order matters, since buffering a compressed stream differs from compressing a buffered one, and that closing the outermost stream tears down the whole chain because each decorator's close() delegates inward.
Remember one sentence: each Java I/O decorator wraps another stream, adds one feature, and avoids subclass explosion by composing capabilities at runtime.
---