Limited time: AI code review, hints, mock interviews, whiteboard analysis, and all Pro features are unlocked. Enroll
โฑ๏ธ 8 min read

Decorator Pattern

The Decorator pattern attaches additional behavior to an object dynamically by wrapping it. Each decorator implements the same interface as the component it wraps, forwarding calls while adding its own logic before or after. You can stack multiple decorators to compose complex behavior from simple pieces.

Why this matters: When you need to add cross-cutting concerns (caching, logging, retry, compression, encryption) without polluting the core logic, Decorator is the cleanest approach. Multi-level caching is literally stacking decorators.


Prerequisites


Class Diagram

classDiagram
    class DataSource {
        <<interface>>
        +read(key) String
        +write(key, value)
    }
    class DatabaseSource {
        +read(key) String
        +write(key, value)
    }
    class CachingDecorator {
        -wrapped: DataSource
        -cache: Map
        +read(key) String
        +write(key, value)
    }
    class LoggingDecorator {
        -wrapped: DataSource
        +read(key) String
        +write(key, value)
    }
    class CompressionDecorator {
        -wrapped: DataSource
        +read(key) String
        +write(key, value)
    }

    DataSource <|.. DatabaseSource
    DataSource <|.. CachingDecorator
    DataSource <|.. LoggingDecorator
    DataSource <|.. CompressionDecorator
    CachingDecorator --> DataSource
    LoggingDecorator --> DataSource
    CompressionDecorator --> DataSource


Real-World Example: Multi-Level Cache

// Component interface
public interface DataSource {
    String read(String key);
    void write(String key, String value);
}

// Concrete component: actual database
public class DatabaseSource implements DataSource {
    private final JdbcTemplate db;

    public DatabaseSource(JdbcTemplate db) { this.db = db; }

    @Override
    public String read(String key) {
        return db.queryForObject("SELECT value FROM kv WHERE key = ?", String.class, key);
    }

    @Override
    public void write(String key, String value) {
        db.update("INSERT INTO kv (key, value) VALUES (?, ?) ON CONFLICT UPDATE SET value = ?",
                  key, value, value);
    }
}

// Decorator: in-memory L1 cache
public class L1CacheDecorator implements DataSource {
    private final DataSource wrapped;
    private final Map<String, CacheEntry> cache;
    private final Duration ttl;

    public L1CacheDecorator(DataSource wrapped, int maxSize, Duration ttl) {
        this.wrapped = wrapped;
        this.cache = new LinkedHashMap<>(maxSize, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry<String, CacheEntry> eldest) {
                return size() > maxSize;
            }
        };
        this.ttl = ttl;
    }

    @Override
    public String read(String key) {
        CacheEntry entry = cache.get(key);
        if (entry != null && !entry.isExpired(ttl)) {
            return entry.getValue(); // L1 hit
        }
        String value = wrapped.read(key); // Delegate to next layer
        if (value != null) {
            cache.put(key, new CacheEntry(value, Instant.now()));
        }
        return value;
    }

    @Override
    public void write(String key, String value) {
        cache.remove(key); // Invalidate L1
        wrapped.write(key, value); // Delegate
    }
}

// Decorator: Redis L2 cache
public class L2RedisCacheDecorator implements DataSource {
    private final DataSource wrapped;
    private final RedisClient redis;
    private final Duration ttl;

    public L2RedisCacheDecorator(DataSource wrapped, RedisClient redis, Duration ttl) {
        this.wrapped = wrapped;
        this.redis = redis;
        this.ttl = ttl;
    }

    @Override
    public String read(String key) {
        String cached = redis.get(key);
        if (cached != null) return cached; // L2 hit
        
        String value = wrapped.read(key); // Miss -- go to DB
        if (value != null) {
            redis.setex(key, ttl.getSeconds(), value);
        }
        return value;
    }

    @Override
    public void write(String key, String value) {
        redis.del(key); // Invalidate L2
        wrapped.write(key, value);
    }
}

// Decorator: logging
public class LoggingDecorator implements DataSource {
    private final DataSource wrapped;
    private final Logger logger;

    public LoggingDecorator(DataSource wrapped, Logger logger) {
        this.wrapped = wrapped;
        this.logger = logger;
    }

    @Override
    public String read(String key) {
        long start = System.nanoTime();
        String result = wrapped.read(key);
        long elapsed = System.nanoTime() - start;
        logger.debug("READ key={} elapsed={}ms hit={}", key, elapsed / 1_000_000, result != null);
        return result;
    }

    @Override
    public void write(String key, String value) {
        wrapped.write(key, value);
        logger.info("WRITE key={} size={}", key, value.length());
    }
}

// Compose decorators: L1 -> L2 -> DB (with logging around the whole stack)
DataSource db = new DatabaseSource(jdbc);
DataSource withL2 = new L2RedisCacheDecorator(db, redis, Duration.ofMinutes(5));
DataSource withL1 = new L1CacheDecorator(withL2, 1000, Duration.ofSeconds(30));
DataSource dataSource = new LoggingDecorator(withL1, logger);

// Client uses dataSource -- doesn't know about caching layers
String value = dataSource.read("user:123");
from abc import ABC, abstractmethod
from functools import lru_cache
import time

class DataSource(ABC):
    @abstractmethod
    def read(self, key: str) -> str | None: ...
    @abstractmethod
    def write(self, key: str, value: str): ...

class DatabaseSource(DataSource):
    def __init__(self, connection):
        self._conn = connection

    def read(self, key: str) -> str | None:
        row = self._conn.execute("SELECT value FROM kv WHERE key = ?", (key,)).fetchone()
        return row[0] if row else None

    def write(self, key: str, value: str):
        self._conn.execute("INSERT OR REPLACE INTO kv (key, value) VALUES (?, ?)", (key, value))

class L1CacheDecorator(DataSource):
    def __init__(self, wrapped: DataSource, max_size: int = 1000, ttl_seconds: float = 30):
        self._wrapped = wrapped
        self._cache: dict[str, tuple[str, float]] = {}
        self._max_size = max_size
        self._ttl = ttl_seconds

    def read(self, key: str) -> str | None:
        if key in self._cache:
            value, timestamp = self._cache[key]
            if time.time() - timestamp < self._ttl:
                return value  # L1 hit
            del self._cache[key]
        
        value = self._wrapped.read(key)
        if value is not None:
            if len(self._cache) >= self._max_size:
                oldest = next(iter(self._cache))
                del self._cache[oldest]
            self._cache[key] = (value, time.time())
        return value

    def write(self, key: str, value: str):
        self._cache.pop(key, None)  # Invalidate
        self._wrapped.write(key, value)

class LoggingDecorator(DataSource):
    def __init__(self, wrapped: DataSource):
        self._wrapped = wrapped

    def read(self, key: str) -> str | None:
        start = time.perf_counter()
        result = self._wrapped.read(key)
        elapsed = (time.perf_counter() - start) * 1000
        print(f"READ key={key} elapsed={elapsed:.2f}ms hit={result is not None}")
        return result

    def write(self, key: str, value: str):
        self._wrapped.write(key, value)
        print(f"WRITE key={key} size={len(value)}")

# Compose: logging -> L1 cache -> database
db = DatabaseSource(connection)
cached = L1CacheDecorator(db, max_size=500, ttl_seconds=60)
data_source = LoggingDecorator(cached)
class DataSource {
public:
    virtual ~DataSource() = default;
    virtual optional<string> read(const string& key) = 0;
    virtual void write(const string& key, const string& value) = 0;
};

class DatabaseSource : public DataSource {
public:
    optional<string> read(const string& key) override {
        // DB query
        return db_.query(key);
    }
    void write(const string& key, const string& value) override {
        db_.upsert(key, value);
    }
};

class CachingDecorator : public DataSource {
    unique_ptr<DataSource> wrapped_;
    unordered_map<string, pair<string, TimePoint>> cache_;
    chrono::seconds ttl_;
public:
    CachingDecorator(unique_ptr<DataSource> wrapped, chrono::seconds ttl)
        : wrapped_(std::move(wrapped)), ttl_(ttl) {}

    optional<string> read(const string& key) override {
        auto it = cache_.find(key);
        if (it != cache_.end()) {
            auto age = Clock::now() - it->second.second;
            if (age < ttl_) return it->second.first;
            cache_.erase(it);
        }
        auto value = wrapped_->read(key);
        if (value) cache_[key] = {*value, Clock::now()};
        return value;
    }

    void write(const string& key, const string& value) override {
        cache_.erase(key);
        wrapped_->write(key, value);
    }
};

class LoggingDecorator : public DataSource {
    unique_ptr<DataSource> wrapped_;
public:
    explicit LoggingDecorator(unique_ptr<DataSource> wrapped)
        : wrapped_(std::move(wrapped)) {}

    optional<string> read(const string& key) override {
        auto start = Clock::now();
        auto result = wrapped_->read(key);
        auto elapsed = chrono::duration_cast<chrono::milliseconds>(Clock::now() - start);
        cout << "READ key=" << key << " elapsed=" << elapsed.count() << "ms" << endl;
        return result;
    }

    void write(const string& key, const string& value) override {
        wrapped_->write(key, value);
        cout << "WRITE key=" << key << endl;
    }
};

// Compose
auto source = make_unique<LoggingDecorator>(
    make_unique<CachingDecorator>(
        make_unique<DatabaseSource>(),
        chrono::seconds(60)
    )
);

Decorator vs Inheritance

Decorator Inheritance
Add behavior at runtime Behavior fixed at compile time
Compose multiple behaviors freely Leads to class explosion (LoggingCachingCompressingDB)
Same interface as wrapped object Tightly coupled to parent class
Can be removed/replaced at runtime Permanent once compiled

When to Use vs When to Avoid

Use Decorator When Avoid When
Adding cross-cutting concerns (cache, log, retry, auth) The core behavior is what changes (use Strategy instead)
Behaviors need to be composed in different combinations Only one combination will ever exist
You want to add/remove behavior at runtime Behavior is permanently required
Subclassing would create a combinatorial explosion One or two fixed subclasses suffice

Interview Questions

  1. โ€œDecorator vs Proxy โ€“ whatโ€™s the difference?โ€ โ€“ Decorator adds new behavior. Proxy controls access to an existing object (lazy loading, access control, remote proxy). Similar structure, different intent.

  2. โ€œHow do you order decorators?โ€ โ€“ Order matters. Cache(Log(DB)) logs cache misses only. Log(Cache(DB)) logs everything. Think about what behavior you want at each layer.

  3. โ€œDoesnโ€™t this create too many small classes?โ€ โ€“ Yes, thatโ€™s the tradeoff. But each class is simple, testable, and reusable. Compare to one massive class that does caching + logging + compression.

  4. โ€œHow does this relate to Javaโ€™s I/O streams?โ€ โ€“ BufferedInputStream(new FileInputStream(file)) is exactly Decorator. Each stream wraps another, adding buffering, compression, or encryption.


See It in Action

Multilevel Cache

Free system design + DSA prep. If it helped you crack an interview, consider supporting.

SensAI SensAI
Beta
Listening...
Tap mic to stop voice mode

Shape what we build next

Every piece of feedback is read by the team and directly influences our roadmap.

What type of feedback?

Install SystemCraft

Add to your home screen for instant access, offline reading, and a distraction-free experience.

Offline reading Faster loads No browser tabs App-like feel

Unlock AI Features

One click to activate - no payment, no credit card. Just sign in and you're in.

AI code review and hints
SensAI chat assistant
AI mock interviews
Whiteboard analysis
100% free during early access