Decorator and Proxy Patterns Explained

Introduction
In the previous post, you learned two structural patterns that wrap existing objects: Adapter converts an interface, Facade simplifies a subsystem. Now we'll look at two more structural wrappers — but with very different goals:
| Pattern | One-Line Summary |
|---|---|
| Decorator | Adds new behavior to an object dynamically, without modifying its class |
| Proxy | Controls access to an object — lazy loading, caching, access control, logging |
Both patterns wrap an object and implement the same interface. The difference is intent: Decorator extends what an object can do. Proxy controls how an object is accessed.
What You'll Learn
✅ Add behavior dynamically with the Decorator pattern — no subclass explosion
✅ Stack multiple decorators to compose complex behavior
✅ Control object access with Proxy — lazy loading, caching, protection, logging
✅ Implement both patterns in TypeScript, Python, and Java
✅ Distinguish Decorator vs Proxy vs Adapter — three wrapping patterns, three different purposes
✅ Avoid common pitfalls when wrapping objects
Prerequisites
- Completed Adapter and Facade Patterns
- Familiar with Polymorphism and Interfaces
- Comfortable with Inheritance and Composition
Part 1: The Decorator Pattern
The Problem: Subclass Explosion
You're building a notification system. You start with a simple Notifier that sends emails:
class EmailNotifier {
send(message: string): void {
console.log(`Email: ${message}`);
}
}Then requirements grow:
- Some users also want SMS notifications
- Some want Slack notifications
- Some want email + SMS
- Some want email + Slack
- Some want all three
With inheritance, you'd need a subclass for every combination:
// ❌ Subclass explosion — 2^n combinations
class EmailNotifier { /* ... */ }
class SMSNotifier extends EmailNotifier { /* ... */ }
class SlackNotifier extends EmailNotifier { /* ... */ }
class EmailAndSMSNotifier extends EmailNotifier { /* ... */ }
class EmailAndSlackNotifier extends EmailNotifier { /* ... */ }
class SMSAndSlackNotifier extends EmailNotifier { /* ... */ }
class EmailSMSAndSlackNotifier extends EmailNotifier { /* ... */ }
// Add push notifications? Now 2^4 = 16 classes ❌Three notification channels = 7 classes. Four channels = 15 classes. This doesn't scale.
The Decorator pattern solves this: wrap objects with additional behavior at runtime, stacking as many as you need.
Pattern Structure
Participants:
- Component — the interface shared by all objects and decorators (
Notifier) - ConcreteComponent — the base object being decorated (
EmailNotifier) - BaseDecorator — holds a reference to a wrapped component, delegates calls to it
- ConcreteDecorators — extend the base decorator and add specific behavior (
SMSDecorator,SlackDecorator)
Key insight: Because decorators implement the same interface as the component, you can stack them indefinitely. Each decorator wraps the previous one like Russian nesting dolls.
TypeScript Implementation
Notification System
// Component interface
interface Notifier {
send(message: string): void;
}
// Concrete component — the base object
class EmailNotifier implements Notifier {
constructor(private email: string) {}
send(message: string): void {
console.log(`📧 Email to ${this.email}: ${message}`);
}
}
// Base decorator — implements same interface, wraps a component
abstract class NotifierDecorator implements Notifier {
constructor(protected wrapped: Notifier) {}
send(message: string): void {
this.wrapped.send(message);
}
}
// Concrete decorators — each adds one behavior
class SMSDecorator extends NotifierDecorator {
constructor(wrapped: Notifier, private phone: string) {
super(wrapped);
}
send(message: string): void {
super.send(message); // delegate to wrapped
console.log(`📱 SMS to ${this.phone}: ${message}`);
}
}
class SlackDecorator extends NotifierDecorator {
constructor(wrapped: Notifier, private channel: string) {
super(wrapped);
}
send(message: string): void {
super.send(message); // delegate to wrapped
console.log(`💬 Slack #${this.channel}: ${message}`);
}
}
class PushDecorator extends NotifierDecorator {
constructor(wrapped: Notifier, private deviceId: string) {
super(wrapped);
}
send(message: string): void {
super.send(message); // delegate to wrapped
console.log(`🔔 Push to ${this.deviceId}: ${message}`);
}
}
// Usage — stack decorators based on user preferences
let notifier: Notifier = new EmailNotifier("alice@example.com");
// User wants email + SMS + Slack
notifier = new SMSDecorator(notifier, "+1234567890");
notifier = new SlackDecorator(notifier, "alerts");
notifier.send("Server is down!");
// Output:
// 📧 Email to alice@example.com: Server is down!
// 📱 SMS to +1234567890: Server is down!
// 💬 Slack #alerts: Server is down!Key distinction from Adapter: An Adapter changes the interface (converts A to B). A Decorator keeps the same interface and adds behavior on top.
Coffee Shop Example (Classic)
The classic Decorator example — building coffee orders by stacking add-ons:
interface Coffee {
getCost(): number;
getDescription(): string;
}
class SimpleCoffee implements Coffee {
getCost(): number { return 5; }
getDescription(): string { return "Simple coffee"; }
}
class Espresso implements Coffee {
getCost(): number { return 6; }
getDescription(): string { return "Espresso"; }
}
// Base decorator
abstract class CoffeeDecorator implements Coffee {
constructor(protected coffee: Coffee) {}
getCost(): number { return this.coffee.getCost(); }
getDescription(): string { return this.coffee.getDescription(); }
}
class MilkDecorator extends CoffeeDecorator {
getCost(): number { return this.coffee.getCost() + 2; }
getDescription(): string { return this.coffee.getDescription() + ", milk"; }
}
class SugarDecorator extends CoffeeDecorator {
getCost(): number { return this.coffee.getCost() + 1; }
getDescription(): string { return this.coffee.getDescription() + ", sugar"; }
}
class WhipDecorator extends CoffeeDecorator {
getCost(): number { return this.coffee.getCost() + 3; }
getDescription(): string { return this.coffee.getDescription() + ", whipped cream"; }
}
// Order: Espresso with milk and whipped cream
let order: Coffee = new Espresso();
order = new MilkDecorator(order);
order = new WhipDecorator(order);
console.log(order.getDescription()); // "Espresso, milk, whipped cream"
console.log(`$${order.getCost()}`); // "$11"Python Implementation
from abc import ABC, abstractmethod
# Component interface
class Notifier(ABC):
@abstractmethod
def send(self, message: str) -> None:
pass
# Concrete component
class EmailNotifier(Notifier):
def __init__(self, email: str):
self._email = email
def send(self, message: str) -> None:
print(f"📧 Email to {self._email}: {message}")
# Base decorator
class NotifierDecorator(Notifier):
def __init__(self, wrapped: Notifier):
self._wrapped = wrapped
def send(self, message: str) -> None:
self._wrapped.send(message)
# Concrete decorators
class SMSDecorator(NotifierDecorator):
def __init__(self, wrapped: Notifier, phone: str):
super().__init__(wrapped)
self._phone = phone
def send(self, message: str) -> None:
super().send(message)
print(f"📱 SMS to {self._phone}: {message}")
class SlackDecorator(NotifierDecorator):
def __init__(self, wrapped: Notifier, channel: str):
super().__init__(wrapped)
self._channel = channel
def send(self, message: str) -> None:
super().send(message)
print(f"💬 Slack #{self._channel}: {message}")
# Usage
notifier: Notifier = EmailNotifier("alice@example.com")
notifier = SMSDecorator(notifier, "+1234567890")
notifier = SlackDecorator(notifier, "alerts")
notifier.send("Server is down!")
# 📧 Email to alice@example.com: Server is down!
# 📱 SMS to +1234567890: Server is down!
# 💬 Slack #alerts: Server is down!Python-Specific: Function Decorators
Python has built-in decorator syntax (@decorator) — which is actually the Decorator pattern applied to functions:
import time
import functools
def timer(func):
"""Decorator that measures execution time"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
def retry(max_attempts: int = 3):
"""Decorator that retries on failure"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts:
raise
print(f"Attempt {attempt} failed: {e}. Retrying...")
return wrapper
return decorator
# Stack decorators — same concept as class-based decorators
@timer
@retry(max_attempts=3)
def fetch_data(url: str) -> dict:
print(f"Fetching {url}...")
return {"status": "ok"}
fetch_data("https://api.example.com/data")
# Fetching https://api.example.com/data...
# fetch_data took 0.0001sSame concept, different syntax. Python's
@decoratoris syntactic sugar for the Decorator pattern. The@timerwrapsfetch_datajust likeSMSDecoratorwrapsEmailNotifier.
Java Implementation
// Component interface
public interface Notifier {
void send(String message);
}
// Concrete component
public class EmailNotifier implements Notifier {
private final String email;
public EmailNotifier(String email) {
this.email = email;
}
@Override
public void send(String message) {
System.out.printf("📧 Email to %s: %s%n", email, message);
}
}
// Base decorator
public abstract class NotifierDecorator implements Notifier {
protected final Notifier wrapped;
public NotifierDecorator(Notifier wrapped) {
this.wrapped = wrapped;
}
@Override
public void send(String message) {
wrapped.send(message);
}
}
// Concrete decorators
public class SMSDecorator extends NotifierDecorator {
private final String phone;
public SMSDecorator(Notifier wrapped, String phone) {
super(wrapped);
this.phone = phone;
}
@Override
public void send(String message) {
super.send(message);
System.out.printf("📱 SMS to %s: %s%n", phone, message);
}
}
public class SlackDecorator extends NotifierDecorator {
private final String channel;
public SlackDecorator(Notifier wrapped, String channel) {
super(wrapped);
this.channel = channel;
}
@Override
public void send(String message) {
super.send(message);
System.out.printf("💬 Slack #%s: %s%n", channel, message);
}
}
// Usage
Notifier notifier = new EmailNotifier("alice@example.com");
notifier = new SMSDecorator(notifier, "+1234567890");
notifier = new SlackDecorator(notifier, "alerts");
notifier.send("Server is down!");Java Standard Library: java.io Stream Decorators
Java's I/O library is built entirely on the Decorator pattern:
// Each wrapper adds one capability — classic Decorator stacking
InputStream raw = new FileInputStream("data.gz"); // base component
InputStream buffered = new BufferedInputStream(raw); // + buffering
InputStream decompressed = new GZIPInputStream(buffered); // + decompression
Reader reader = new InputStreamReader(decompressed, "UTF-8"); // + char decoding
BufferedReader lines = new BufferedReader(reader); // + line reading
String line;
while ((line = lines.readLine()) != null) {
System.out.println(line);
}Real-world Decorator in action. Every Java developer uses this pattern daily —
BufferedReader,GZIPInputStream,DataInputStreamare all decorators wrapping a baseInputStream.
Real-World Decorator Examples
HTTP Middleware (Express.js / Hono)
Middleware in web frameworks is the Decorator pattern applied to request handlers:
// Each middleware "decorates" the request pipeline
interface Handler {
handle(request: Request): Response;
}
class BaseHandler implements Handler {
handle(request: Request): Response {
return new Response("OK", { status: 200 });
}
}
class LoggingMiddleware implements Handler {
constructor(private next: Handler) {}
handle(request: Request): Response {
console.log(`${request.method} ${request.url}`);
const start = Date.now();
const response = this.next.handle(request);
console.log(`Completed in ${Date.now() - start}ms`);
return response;
}
}
class AuthMiddleware implements Handler {
constructor(private next: Handler) {}
handle(request: Request): Response {
const token = request.headers.get("Authorization");
if (!token) {
return new Response("Unauthorized", { status: 401 });
}
return this.next.handle(request);
}
}
class RateLimitMiddleware implements Handler {
private requests = new Map<string, number>();
constructor(private next: Handler, private limit: number = 100) {}
handle(request: Request): Response {
const ip = request.headers.get("X-Forwarded-For") ?? "unknown";
const count = (this.requests.get(ip) ?? 0) + 1;
this.requests.set(ip, count);
if (count > this.limit) {
return new Response("Too Many Requests", { status: 429 });
}
return this.next.handle(request);
}
}
// Stack middleware — order matters!
let handler: Handler = new BaseHandler();
handler = new AuthMiddleware(handler); // check auth first
handler = new RateLimitMiddleware(handler); // rate limit before auth
handler = new LoggingMiddleware(handler); // log everythingPart 2: The Proxy Pattern
The Problem: Controlling Object Access
You have a ReportGenerator that queries a database, processes millions of rows, and generates a PDF. It takes 30 seconds to create:
class ReportGenerator {
constructor() {
// Connects to database, loads configuration...
console.log("Initializing report generator (30 seconds)...");
}
generate(reportType: string): Buffer {
console.log(`Generating ${reportType} report...`);
// Heavy processing
return Buffer.from("PDF content");
}
}Problems with using this directly:
- Startup cost — the object is created even if the user never requests a report
- No caching — generating the same report twice is wasteful
- No access control — any user can generate any report
- No logging — you can't track who generates what
The Proxy pattern wraps the real object and adds control without changing its interface.
Pattern Structure
Participants:
- Subject — the interface shared by the real object and the proxy (
ReportService) - RealSubject — the actual object that does the work (
ReportGenerator) - Proxy — wraps the real subject, controls access to it (
ReportProxy)
Types of Proxies
| Proxy Type | Purpose | Example |
|---|---|---|
| Virtual Proxy | Delays creation until first use (lazy loading) | Load heavy images only when displayed |
| Protection Proxy | Controls access based on permissions | Check user role before allowing operation |
| Caching Proxy | Caches results to avoid repeating expensive work | Cache API responses for 5 minutes |
| Logging Proxy | Records operations for monitoring/debugging | Log every database query |
TypeScript Implementation
Virtual Proxy (Lazy Loading)
interface Image {
display(): void;
getSize(): { width: number; height: number };
}
// Real subject — expensive to create
class HighResImage implements Image {
private data: Buffer;
constructor(private filename: string) {
// Simulates loading a 50MB image from disk
console.log(`Loading ${filename} from disk... (slow)`);
this.data = Buffer.alloc(50 * 1024 * 1024); // 50MB
}
display(): void {
console.log(`Displaying ${this.filename} (${this.data.length} bytes)`);
}
getSize(): { width: number; height: number } {
return { width: 3840, height: 2160 };
}
}
// Virtual proxy — delays loading until display() is called
class ImageProxy implements Image {
private realImage?: HighResImage;
constructor(private filename: string) {
// No loading here — just stores the filename
console.log(`Created proxy for ${filename} (no disk I/O yet)`);
}
display(): void {
if (!this.realImage) {
this.realImage = new HighResImage(this.filename); // load on first use
}
this.realImage.display();
}
getSize(): { width: number; height: number } {
// Can return metadata without loading the full image
return { width: 3840, height: 2160 };
}
}
// Usage — create 100 proxies instantly, only load when displayed
const gallery: Image[] = [];
for (let i = 0; i < 100; i++) {
gallery.push(new ImageProxy(`photo_${i}.jpg`)); // instant — no disk I/O
}
// Only this one loads from disk
gallery[42].display();
// Created proxy for photo_42.jpg (no disk I/O yet)
// Loading photo_42.jpg from disk... (slow)
// Displaying photo_42.jpg (52428800 bytes)Caching Proxy
interface WeatherService {
getWeather(city: string): WeatherData;
}
interface WeatherData {
city: string;
temperature: number;
humidity: number;
fetchedAt: Date;
}
// Real subject — calls external API (slow, rate-limited)
class OpenWeatherService implements WeatherService {
getWeather(city: string): WeatherData {
console.log(`Calling OpenWeather API for ${city}...`);
// Simulate API call
return {
city,
temperature: 20 + Math.random() * 15,
humidity: 40 + Math.random() * 40,
fetchedAt: new Date(),
};
}
}
// Caching proxy — caches results for a configurable duration
class CachedWeatherService implements WeatherService {
private cache = new Map<string, { data: WeatherData; expiresAt: number }>();
constructor(
private realService: WeatherService,
private ttlMs: number = 5 * 60 * 1000 // 5 minutes default
) {}
getWeather(city: string): WeatherData {
const cached = this.cache.get(city);
if (cached && Date.now() < cached.expiresAt) {
console.log(`Cache hit for ${city}`);
return cached.data;
}
console.log(`Cache miss for ${city}`);
const data = this.realService.getWeather(city);
this.cache.set(city, { data, expiresAt: Date.now() + this.ttlMs });
return data;
}
}
// Usage
const weather: WeatherService = new CachedWeatherService(
new OpenWeatherService(),
5 * 60 * 1000 // cache for 5 minutes
);
weather.getWeather("London"); // Cache miss → API call
weather.getWeather("London"); // Cache hit → instant
weather.getWeather("Paris"); // Cache miss → API call
weather.getWeather("London"); // Cache hit → instantProtection Proxy (Access Control)
interface DocumentService {
read(docId: string): string;
write(docId: string, content: string): void;
delete(docId: string): void;
}
type Role = "viewer" | "editor" | "admin";
// Real subject
class DocumentStore implements DocumentService {
private docs = new Map<string, string>();
read(docId: string): string {
return this.docs.get(docId) ?? "Document not found";
}
write(docId: string, content: string): void {
this.docs.set(docId, content);
console.log(`Document ${docId} saved`);
}
delete(docId: string): void {
this.docs.delete(docId);
console.log(`Document ${docId} deleted`);
}
}
// Protection proxy — enforces role-based access
class SecureDocumentService implements DocumentService {
constructor(
private realService: DocumentService,
private userRole: Role
) {}
read(docId: string): string {
// All roles can read
return this.realService.read(docId);
}
write(docId: string, content: string): void {
if (this.userRole === "viewer") {
throw new Error("Access denied: viewers cannot write documents");
}
this.realService.write(docId, content);
}
delete(docId: string): void {
if (this.userRole !== "admin") {
throw new Error("Access denied: only admins can delete documents");
}
this.realService.delete(docId);
}
}
// Usage
const adminDocs: DocumentService = new SecureDocumentService(
new DocumentStore(), "admin"
);
adminDocs.write("doc1", "Hello"); // ✅ works
adminDocs.delete("doc1"); // ✅ works
const viewerDocs: DocumentService = new SecureDocumentService(
new DocumentStore(), "viewer"
);
viewerDocs.read("doc1"); // ✅ works
viewerDocs.write("doc1", "Hack"); // ❌ throws "Access denied"Logging Proxy
interface Database {
query(sql: string): unknown[];
execute(sql: string): void;
}
class PostgresDatabase implements Database {
query(sql: string): unknown[] {
// Execute query
return [{ id: 1, name: "Alice" }];
}
execute(sql: string): void {
// Execute statement
}
}
// Logging proxy — records every operation
class LoggingDatabase implements Database {
constructor(private realDb: Database) {}
query(sql: string): unknown[] {
const start = performance.now();
const result = this.realDb.query(sql);
const elapsed = (performance.now() - start).toFixed(2);
console.log(`[QUERY] ${sql} → ${result.length} rows (${elapsed}ms)`);
return result;
}
execute(sql: string): void {
const start = performance.now();
this.realDb.execute(sql);
const elapsed = (performance.now() - start).toFixed(2);
console.log(`[EXEC] ${sql} (${elapsed}ms)`);
}
}
// Usage — swap in logging during development
const db: Database = new LoggingDatabase(new PostgresDatabase());
db.query("SELECT * FROM users WHERE role = 'admin'");
// [QUERY] SELECT * FROM users WHERE role = 'admin' → 1 rows (0.12ms)Python Implementation
from abc import ABC, abstractmethod
from functools import lru_cache
import time
# ---- Virtual Proxy ----
class Image(ABC):
@abstractmethod
def display(self) -> None:
pass
class HighResImage(Image):
def __init__(self, filename: str):
self._filename = filename
self._load_from_disk()
def _load_from_disk(self) -> None:
print(f"Loading {self._filename} from disk... (slow)")
time.sleep(0.1) # simulate slow I/O
def display(self) -> None:
print(f"Displaying {self._filename}")
class ImageProxy(Image):
def __init__(self, filename: str):
self._filename = filename
self._real_image: HighResImage | None = None
print(f"Created proxy for {self._filename} (no disk I/O yet)")
def display(self) -> None:
if self._real_image is None:
self._real_image = HighResImage(self._filename)
self._real_image.display()
# Usage
gallery = [ImageProxy(f"photo_{i}.jpg") for i in range(100)] # instant
gallery[42].display() # only this one loads
# ---- Caching Proxy ----
class WeatherService(ABC):
@abstractmethod
def get_weather(self, city: str) -> dict:
pass
class OpenWeatherService(WeatherService):
def get_weather(self, city: str) -> dict:
print(f"Calling API for {city}...")
return {"city": city, "temp": 22.5, "humidity": 65}
class CachedWeatherService(WeatherService):
def __init__(self, real_service: WeatherService, ttl_seconds: float = 300):
self._real_service = real_service
self._ttl = ttl_seconds
self._cache: dict[str, tuple[dict, float]] = {}
def get_weather(self, city: str) -> dict:
cached = self._cache.get(city)
if cached and time.time() < cached[1]:
print(f"Cache hit for {city}")
return cached[0]
print(f"Cache miss for {city}")
data = self._real_service.get_weather(city)
self._cache[city] = (data, time.time() + self._ttl)
return data
# Usage
weather = CachedWeatherService(OpenWeatherService(), ttl_seconds=300)
weather.get_weather("London") # cache miss → API call
weather.get_weather("London") # cache hit → instant
# ---- Protection Proxy ----
class DocumentService(ABC):
@abstractmethod
def read(self, doc_id: str) -> str:
pass
@abstractmethod
def write(self, doc_id: str, content: str) -> None:
pass
@abstractmethod
def delete(self, doc_id: str) -> None:
pass
class DocumentStore(DocumentService):
def __init__(self):
self._docs: dict[str, str] = {}
def read(self, doc_id: str) -> str:
return self._docs.get(doc_id, "Not found")
def write(self, doc_id: str, content: str) -> None:
self._docs[doc_id] = content
print(f"Document {doc_id} saved")
def delete(self, doc_id: str) -> None:
self._docs.pop(doc_id, None)
print(f"Document {doc_id} deleted")
class SecureDocumentService(DocumentService):
def __init__(self, real_service: DocumentService, user_role: str):
self._real = real_service
self._role = user_role
def read(self, doc_id: str) -> str:
return self._real.read(doc_id)
def write(self, doc_id: str, content: str) -> None:
if self._role == "viewer":
raise PermissionError("Viewers cannot write documents")
self._real.write(doc_id, content)
def delete(self, doc_id: str) -> None:
if self._role != "admin":
raise PermissionError("Only admins can delete documents")
self._real.delete(doc_id)Java Implementation
// ---- Virtual Proxy ----
public interface Image {
void display();
}
public class HighResImage implements Image {
private final String filename;
public HighResImage(String filename) {
this.filename = filename;
loadFromDisk();
}
private void loadFromDisk() {
System.out.printf("Loading %s from disk... (slow)%n", filename);
}
@Override
public void display() {
System.out.printf("Displaying %s%n", filename);
}
}
public class ImageProxy implements Image {
private final String filename;
private HighResImage realImage;
public ImageProxy(String filename) {
this.filename = filename;
System.out.printf("Created proxy for %s (no disk I/O yet)%n", filename);
}
@Override
public void display() {
if (realImage == null) {
realImage = new HighResImage(filename);
}
realImage.display();
}
}
// Usage
List<Image> gallery = new ArrayList<>();
for (int i = 0; i < 100; i++) {
gallery.add(new ImageProxy("photo_" + i + ".jpg")); // instant
}
gallery.get(42).display(); // only this one loads// ---- Caching Proxy ----
public interface WeatherService {
WeatherData getWeather(String city);
}
public record WeatherData(String city, double temperature, double humidity) {}
public class OpenWeatherService implements WeatherService {
@Override
public WeatherData getWeather(String city) {
System.out.printf("Calling API for %s...%n", city);
return new WeatherData(city, 22.5, 65);
}
}
public class CachedWeatherService implements WeatherService {
private final WeatherService realService;
private final long ttlMillis;
private final Map<String, CacheEntry> cache = new HashMap<>();
private record CacheEntry(WeatherData data, long expiresAt) {}
public CachedWeatherService(WeatherService realService, long ttlMillis) {
this.realService = realService;
this.ttlMillis = ttlMillis;
}
@Override
public WeatherData getWeather(String city) {
CacheEntry cached = cache.get(city);
if (cached != null && System.currentTimeMillis() < cached.expiresAt()) {
System.out.printf("Cache hit for %s%n", city);
return cached.data();
}
System.out.printf("Cache miss for %s%n", city);
WeatherData data = realService.getWeather(city);
cache.put(city, new CacheEntry(data, System.currentTimeMillis() + ttlMillis));
return data;
}
}
// Usage
WeatherService weather = new CachedWeatherService(
new OpenWeatherService(),
5 * 60 * 1000 // 5 minutes
);
weather.getWeather("London"); // cache miss
weather.getWeather("London"); // cache hitJava Standard Library: java.lang.reflect.Proxy
Java has built-in dynamic proxy support — create proxies at runtime without writing a class:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
// Create a logging proxy for ANY interface at runtime
public class LoggingHandler implements InvocationHandler {
private final Object target;
public LoggingHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.printf("[LOG] %s(%s)%n", method.getName(),
args != null ? Arrays.toString(args) : "");
long start = System.nanoTime();
Object result = method.invoke(target, args);
long elapsed = (System.nanoTime() - start) / 1_000_000;
System.out.printf("[LOG] %s returned in %dms%n", method.getName(), elapsed);
return result;
}
}
// Usage — wrap any interface with logging
WeatherService realService = new OpenWeatherService();
WeatherService logged = (WeatherService) Proxy.newProxyInstance(
WeatherService.class.getClassLoader(),
new Class<?>[]{ WeatherService.class },
new LoggingHandler(realService)
);
logged.getWeather("London");
// [LOG] getWeather([London])
// Calling API for London...
// [LOG] getWeather returned in 12msDecorator vs Proxy vs Adapter
All three patterns wrap an object. Here's how to tell them apart:
| Aspect | Decorator | Proxy | Adapter |
|---|---|---|---|
| Intent | Add new behavior | Control access | Convert interface |
| Interface | Same as wrapped | Same as wrapped | Different from wrapped |
| Who creates the wrapped object? | Client passes it in | Proxy creates or manages it | Client passes it in |
| Number of wrappers | Usually stacked (multiple) | Usually one | Usually one |
| When to use | Need to add/combine behaviors flexibly | Need lazy loading, caching, auth, logging | Need to integrate incompatible interfaces |
| Wrapping analogy | Gift wrapping (adds layers) | Security guard (controls access) | Power adapter (converts plug shape) |
Combining Patterns
In practice, you often combine all three:
// Adapter — converts third-party interface
class StripeAdapter implements PaymentService {
constructor(private stripe: StripeSDK) {}
charge(amount: number): Result {
return this.stripe.createCharge(amount * 100); // convert interface
}
}
// Caching Proxy — caches exchange rates
class CachedExchangeRate implements ExchangeRateService {
constructor(private real: ExchangeRateService) {}
getRate(from: string, to: string): number {
// check cache, delegate to real if miss
}
}
// Decorator — adds retry logic to any payment service
class RetryDecorator implements PaymentService {
constructor(private service: PaymentService, private maxRetries: number) {}
charge(amount: number): Result {
for (let i = 0; i < this.maxRetries; i++) {
try { return this.service.charge(amount); }
catch { if (i === this.maxRetries - 1) throw; }
}
}
}
// Compose them all
const payment: PaymentService = new RetryDecorator(
new StripeAdapter(new StripeSDK()),
3
);Common Pitfalls
1. Decorator Breaks When Concrete Type Is Checked
// ❌ Client code checks concrete type — decorators break this
function process(notifier: Notifier) {
if (notifier instanceof EmailNotifier) {
// This fails when notifier is wrapped in decorators!
}
}
// ✅ Use the interface, not the concrete type
function process(notifier: Notifier) {
notifier.send("Hello"); // works with any decorator stack
}2. Too Many Decorator Layers = Debugging Nightmare
// ❌ 10 layers deep — good luck tracing a bug
let service = new BaseService();
service = new LoggingDecorator(service);
service = new CachingDecorator(service);
service = new RetryDecorator(service);
service = new TimeoutDecorator(service);
service = new CircuitBreakerDecorator(service);
service = new MetricsDecorator(service);
service = new TracingDecorator(service);
service = new AuthDecorator(service);
service = new ValidationDecorator(service);
service = new RateLimitDecorator(service);
// ✅ If you have this many concerns, consider a middleware pipeline instead
const pipeline = new Pipeline([
new LoggingMiddleware(),
new AuthMiddleware(),
new RateLimitMiddleware(),
new RetryMiddleware(),
]);3. Proxy That Doesn't Match the Interface Exactly
// ❌ Proxy adds extra methods — clients can't swap proxy for real object
class CachedWeatherProxy implements WeatherService {
getWeather(city: string): WeatherData { /* ... */ }
clearCache(): void { /* ... */ } // ❌ not in the interface
getCacheStats(): Stats { /* ... */ } // ❌ not in the interface
}
// ✅ Keep the proxy interface identical
// Expose cache management through a separate interface if needed
interface CacheControl {
clearCache(): void;
getCacheStats(): Stats;
}
class CachedWeatherProxy implements WeatherService, CacheControl {
// Clients use it as WeatherService — identical to real object
// Admin code can cast to CacheControl when needed
}4. Virtual Proxy With Thread Safety Issues
// ❌ Race condition — two threads can create the real object simultaneously
class ImageProxy implements Image {
private realImage?: HighResImage;
display(): void {
if (!this.realImage) {
// Two threads can enter here at the same time
this.realImage = new HighResImage(this.filename);
}
this.realImage.display();
}
}// ✅ Thread-safe lazy initialization in Java
public class ImageProxy implements Image {
private volatile HighResImage realImage;
@Override
public void display() {
if (realImage == null) {
synchronized (this) {
if (realImage == null) { // double-checked locking
realImage = new HighResImage(filename);
}
}
}
realImage.display();
}
}Summary
The Decorator and Proxy are structural patterns that both wrap objects — but for different reasons:
- Decorator adds new behavior dynamically — it's an enhancer
- Proxy controls access to an object — it's a gatekeeper
Key takeaways:
✅ Use Decorator when you need to add behavior without modifying a class — stack multiple decorators for flexible combinations
✅ Use Proxy for lazy loading (Virtual), access control (Protection), caching, or logging
✅ Both patterns implement the same interface as the wrapped object — clients can't tell the difference
✅ Decorator is typically stacked (multiple layers); Proxy is typically single (one wrapper)
✅ Python's @decorator syntax is the Decorator pattern applied to functions
✅ Java's java.io streams and java.lang.reflect.Proxy are real-world examples in the standard library
What's Next
Up next in the OOP & Design Patterns series:
- OOP-14: Strategy and Template Method Patterns — behavioral patterns for interchangeable algorithms
- OOP-15: Observer, Command, and State Patterns — behavioral patterns for event handling and state machines
Practice Exercises
-
Text Formatting Decorators: Create a
TextComponentinterface withrender(): string. Build decorators forBold,Italic,Underline,Color, andLink. Then compose them to render: a bold, red, underlined link. Verify that removing any decorator still produces valid output. -
API Client with Multiple Proxy Layers: Build an
ApiClientinterface withget(url): Responseandpost(url, body): Response. Create four proxies: (a) CachingProxy that caches GET responses, (b) LoggingProxy that logs all requests, (c) RetryProxy that retries failed requests up to 3 times, and (d) AuthProxy that attaches a bearer token. Stack them in the correct order and explain why the order matters. -
File System Access Control: Create a
FileSysteminterface withread,write,delete, andlistFilesmethods. Implement a Protection Proxy that enforces three roles:guest(read + list only),user(read + write + list),admin(all operations). Write tests that verify each role's permissions. -
Decorator vs Proxy Decision: Given these scenarios, decide which pattern to use and explain why: (a) Adding compression to a network socket, (b) Restricting API access to premium users, (c) Adding encryption to an existing file writer, (d) Caching database query results, (e) Adding transaction support to a repository.
Part 13 of the OOP & Design Patterns series. Builds on Adapter and Facade Patterns.
📬 Subscribe to Newsletter
Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.
We respect your privacy. Unsubscribe at any time.
💬 Comments
Sign in to leave a comment
We'll never post without your permission.