← Back to Dashboard

Decorator Pattern

Laravel: HTTP Middleware pipeline, Cache decorators (tags, prefixes)

Decorator Pattern

The Real-World Analogy

You have a PaymentProcessor class in production. It works. It processes payments. Customers are happy. Then the requirements start stacking up:

  • "We need to log every payment attempt for the audit trail."
  • "We need to cache duplicate payment checks so we don't charge someone twice in 30 seconds."
  • "We need rate limiting so a compromised API key cannot fire 1,000 payments per second."
  • "We need to validate that the amount is positive before processing."

Each of these is a legitimate cross-cutting concern. None of them is the core responsibility of processing a payment. But they all need to happen around every payment call.

Think of it like wrapping a gift. The gift (the base payment processor) does not change. You wrap it in tissue paper (validation), then a box (logging), then gift wrap (caching), then a ribbon (rate limiting). Each layer adds something, but the gift inside is untouched. And you can add or remove layers without affecting the others.

That is the Decorator pattern: wrapping an object with another object that has the same interface, adding behavior before or after delegating to the wrapped object.

The Anti-Pattern: Subclass Explosion

The naive approach to adding these behaviors is inheritance. You create subclasses for each combination:

class PaymentProcessor { /* base */ }
class LoggingPaymentProcessor extends PaymentProcessor { /* adds logging */ }
class CachingPaymentProcessor extends PaymentProcessor { /* adds caching */ }
class ValidatingPaymentProcessor extends PaymentProcessor { /* adds validation */ }

// But wait -- now you need combinations:
class LoggingCachingPaymentProcessor extends LoggingPaymentProcessor { /* ??? */ }
class ValidatingLoggingPaymentProcessor extends ValidatingPaymentProcessor { /* ??? */ }
class CachingValidatingLoggingPaymentProcessor extends ... { /* this is madness */ }

Why this is a problem:

  1. Combinatorial explosion. With 4 behaviors, you need up to 2^4 = 16 subclasses to cover every combination. With 5 behaviors, 32 subclasses. This does not scale.
  2. Rigid ordering. Inheritance hierarchies are fixed at compile time. If you need logging-then-caching in one context and caching-then-logging in another, you need separate class hierarchies.
  3. Tight coupling. Each subclass is permanently bound to its parent. Changing the base class ripples through every subclass.
  4. Single inheritance limit. PHP only supports single inheritance. LoggingCachingPaymentProcessor cannot extend both LoggingPaymentProcessor and CachingPaymentProcessor.

The Fix: Decorators

Instead of subclasses, create wrapper classes that implement the same interface and delegate to the wrapped object:

interface PaymentProcessorInterface
{
    public function process(string $transactionId, float $amount): string;
}

class BasePaymentProcessor implements PaymentProcessorInterface
{
    public function process(string $transactionId, float $amount): string
    {
        // Core payment logic
        return "Processed {$transactionId}: \${$amount}";
    }
}

class LoggingDecorator implements PaymentProcessorInterface
{
    public function __construct(
        private PaymentProcessorInterface $inner
    ) {}

    public function process(string $transactionId, float $amount): string
    {
        echo "[LOG] Starting payment {$transactionId}\n";
        $result = $this->inner->process($transactionId, $amount);
        echo "[LOG] Completed payment {$transactionId}\n";
        return $result;
    }
}

class ValidationDecorator implements PaymentProcessorInterface
{
    public function __construct(
        private PaymentProcessorInterface $inner
    ) {}

    public function process(string $transactionId, float $amount): string
    {
        if ($amount <= 0) {
            throw new \InvalidArgumentException("Amount must be positive, got {$amount}");
        }
        return $this->inner->process($transactionId, $amount);
    }
}

Now you compose behaviors at runtime by nesting decorators:

// Base processor
$processor = new BasePaymentProcessor();

// Wrap with validation
$processor = new ValidationDecorator($processor);

// Wrap with logging
$processor = new LoggingDecorator($processor);

// Use it -- validation and logging happen automatically
$result = $processor->process('TXN-001', 99.50);

Want logging without validation? Skip the ValidationDecorator wrapper. Want to change the order? Swap the nesting. Want to add rate limiting? Create a RateLimitDecorator and wrap it around the existing stack. No existing code changes needed.

The Key Principle: Same Interface, Added Behavior

Every decorator:

  1. Implements the same interface as the object it wraps
  2. Accepts an instance of that interface in its constructor
  3. Delegates to the wrapped object for the core operation
  4. Adds behavior before, after, or around the delegation

This is what makes decorators composable. Because every decorator is also a PaymentProcessorInterface, you can wrap a decorator in another decorator in another decorator. The outermost wrapper does not know (or care) how many layers are below it.

Decorator vs. Adapter vs. Proxy

These three patterns all involve wrapping an object, but they have different intents:

  • Decorator: Same interface, adds behavior. The wrapped object and the wrapper are interchangeable.
  • Adapter: Different interface, translates calls. The wrapped object has an incompatible API.
  • Proxy: Same interface, controls access. The wrapper may add lazy loading, access control, or remote communication without changing behavior.

When to Use the Decorator Pattern

  • You need to add cross-cutting concerns (logging, caching, validation, monitoring, retries) to existing objects without modifying them
  • You need different combinations of behaviors that would cause subclass explosion
  • You want to add or remove behaviors at runtime based on configuration
  • You are building a pipeline where each step wraps the next (middleware)

When NOT to Use It

  • If you only ever need one fixed combination of behaviors, a simple wrapper class is sufficient -- you do not need the full decorator infrastructure
  • If the interface has many methods, every decorator must implement all of them (even if it only cares about one). Consider using a base decorator class that delegates all methods by default
  • Do not use decorators to replace proper composition. If two concerns are always used together, they may belong in the same class

Key takeaway: The Decorator pattern lets you add responsibilities to objects at runtime by wrapping them in layers. Each layer implements the same interface, adds its behavior, and delegates to the wrapped object. This avoids subclass explosion and keeps each concern in its own class.

What You Learned

  • The Decorator pattern adds behavior to an object dynamically by wrapping it in another object that implements the same interface
  • Decorators are stackable — you can layer logging, caching, and validation in any order
  • The original class is never modified, respecting the Open/Closed Principle
  • Decorators are ideal for cross-cutting concerns (logging, caching, auth) that shouldn't pollute core logic

Decorator Pattern in Laravel

HTTP Middleware: Decorators in Action

Laravel's HTTP middleware system is the Decorator pattern in its purest form. Each middleware wraps the next handler in the pipeline, adding behavior before or after the request is processed.

The Middleware Signature

Every Laravel middleware has the same structure:

class VerifyCsrfToken
{
    public function handle(Request $request, Closure $next): Response
    {
        // BEFORE: Add behavior before the request reaches the controller
        if ($this->isReading($request) || $this->tokensMatch($request)) {
            // DELEGATE: Pass the request to the next handler (the wrapped object)
            $response = $next($request);

            // AFTER: Add behavior after the response comes back
            return $this->addCookieToResponse($request, $response);
        }

        throw new TokenMismatchException();
    }
}

This is exactly the Decorator pattern:

  1. The middleware accepts the "next" handler (the $next closure) -- this is the wrapped object
  2. It adds behavior before and/or after calling $next($request) -- this is the decoration
  3. The caller does not know how many middleware layers exist -- the interface is the same at every level

How the Pipeline Works

Laravel's Illuminate\Pipeline\Pipeline class orchestrates the middleware stack. Given middleware [A, B, C] and a controller action, the execution looks like:

A::handle() starts
  B::handle() starts
    C::handle() starts
      Controller action executes
    C::handle() ends
  B::handle() ends
A::handle() ends

Each middleware wraps the next. The controller is the innermost "base object" being decorated. This is identical to nesting decorators:

// Conceptually equivalent to:
$handler = new MiddlewareA(
    new MiddlewareB(
        new MiddlewareC(
            $controllerAction
        )
    )
);

Common Laravel Middleware as Decorators

// Authentication -- guards access (Decorator adds authorization check)
class Authenticate
{
    public function handle($request, Closure $next, ...$guards)
    {
        $this->authenticate($request, $guards);
        return $next($request);  // Delegate to wrapped handler
    }
}

// Throttle -- adds rate limiting (Decorator adds rate limiting behavior)
class ThrottleRequests
{
    public function handle($request, Closure $next, $maxAttempts = 60)
    {
        if ($this->limiter->tooManyAttempts($key, $maxAttempts)) {
            throw new ThrottleRequestsException();
        }

        $response = $next($request);  // Delegate to wrapped handler

        return $this->addHeaders(
            $response, $maxAttempts,
            $this->calculateRemainingAttempts($key, $maxAttempts)
        );
    }
}

// Encrypt Cookies -- transforms data (Decorator modifies request/response)
class EncryptCookies
{
    public function handle($request, Closure $next)
    {
        $request = $this->decrypt($request);     // Before: decrypt incoming
        $response = $next($request);              // Delegate
        return $this->encrypt($response);         // After: encrypt outgoing
    }
}

Each middleware is a decorator. Each wraps the next handler. Each adds a specific concern without modifying the controller or other middleware.

Cache Decorators

Laravel's cache system uses decoration internally. When you use cache tags or prefixes, the base cache store gets wrapped:

// Base store
Cache::store('redis');

// Tagged cache -- decorates the base store with tag-based invalidation
Cache::tags(['users', 'permissions'])->get('user:123');

The TaggedCache class wraps a Repository instance and decorates the get(), put(), and flush() methods with tag-awareness. It delegates the actual storage to the wrapped store but adds tag tracking on top.

// Simplified view of how tagging decorates cache operations
class TaggedCache
{
    public function __construct(
        protected Repository $store,
        protected TagSet $tags,
    ) {}

    public function get(string $key, mixed $default = null): mixed
    {
        // Decoration: prefix the key with tag namespace
        $taggedKey = $this->taggedItemKey($key);

        // Delegation: use the wrapped store for actual retrieval
        return $this->store->get($taggedKey, $default);
    }

    public function flush(): bool
    {
        // Decoration: flush only items with these tags
        $this->tags->reset();
        return true;
    }
}

Writing Your Own Decorator in Laravel

Suppose you have a PaymentGateway interface and you want to add logging to any gateway implementation without modifying it:

// app/Contracts/PaymentGatewayInterface.php
interface PaymentGatewayInterface
{
    public function charge(string $customerId, float $amount): PaymentResult;
}

// app/Services/StripeGateway.php
class StripeGateway implements PaymentGatewayInterface
{
    public function charge(string $customerId, float $amount): PaymentResult
    {
        // Stripe API call
    }
}

// app/Decorators/LoggingGateway.php
class LoggingGateway implements PaymentGatewayInterface
{
    public function __construct(
        private PaymentGatewayInterface $inner,
        private LoggerInterface $logger,
    ) {}

    public function charge(string $customerId, float $amount): PaymentResult
    {
        $this->logger->info("Charging {$customerId}: \${$amount}");

        $result = $this->inner->charge($customerId, $amount);

        $this->logger->info("Charge result for {$customerId}: {$result->status}");

        return $result;
    }
}

Register the decorated version in the container:

// AppServiceProvider
$this->app->bind(PaymentGatewayInterface::class, function ($app) {
    $gateway = new StripeGateway(config('services.stripe.key'));

    return new LoggingGateway($gateway, $app->make(LoggerInterface::class));
});

Every class that depends on PaymentGatewayInterface now automatically gets the logged version. Add more decorators by wrapping further:

$this->app->bind(PaymentGatewayInterface::class, function ($app) {
    $gateway = new StripeGateway(config('services.stripe.key'));
    $logged = new LoggingGateway($gateway, $app->make(LoggerInterface::class));
    $validated = new ValidationGateway($logged);

    return $validated;  // Validates -> Logs -> Processes
});

Key takeaway: Laravel's middleware system is the Decorator pattern. Each middleware wraps the next handler, adding behavior without modifying the controller or other middleware. The same principle applies to cache tags, and you can use it in your own services by binding decorated instances in the container.

This example is pre-filled and runnable. Click "Run" to see the output. Feel free to modify the code and experiment.

Output

                

            

Complete the code below to pass all test cases. Use the hints if you get stuck.