Back to blog

Decorator and Proxy Patterns Explained

oopstructural-patternsdesign-patternstypescriptpythonjava
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:

PatternOne-Line Summary
DecoratorAdds new behavior to an object dynamically, without modifying its class
ProxyControls 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


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.0001s

Same concept, different syntax. Python's @decorator is syntactic sugar for the Decorator pattern. The @timer wraps fetch_data just like SMSDecorator wraps EmailNotifier.


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, DataInputStream are all decorators wrapping a base InputStream.


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 everything

Part 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:

  1. Startup cost — the object is created even if the user never requests a report
  2. No caching — generating the same report twice is wasteful
  3. No access control — any user can generate any report
  4. 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 TypePurposeExample
Virtual ProxyDelays creation until first use (lazy loading)Load heavy images only when displayed
Protection ProxyControls access based on permissionsCheck user role before allowing operation
Caching ProxyCaches results to avoid repeating expensive workCache API responses for 5 minutes
Logging ProxyRecords operations for monitoring/debuggingLog 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 → instant

Protection 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 hit

Java 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 12ms

Decorator vs Proxy vs Adapter

All three patterns wrap an object. Here's how to tell them apart:

AspectDecoratorProxyAdapter
IntentAdd new behaviorControl accessConvert interface
InterfaceSame as wrappedSame as wrappedDifferent from wrapped
Who creates the wrapped object?Client passes it inProxy creates or manages itClient passes it in
Number of wrappersUsually stacked (multiple)Usually oneUsually one
When to useNeed to add/combine behaviors flexiblyNeed lazy loading, caching, auth, loggingNeed to integrate incompatible interfaces
Wrapping analogyGift 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

  1. Text Formatting Decorators: Create a TextComponent interface with render(): string. Build decorators for Bold, Italic, Underline, Color, and Link. Then compose them to render: a bold, red, underlined link. Verify that removing any decorator still produces valid output.

  2. API Client with Multiple Proxy Layers: Build an ApiClient interface with get(url): Response and post(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.

  3. File System Access Control: Create a FileSystem interface with read, write, delete, and listFiles methods. 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.

  4. 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.