Decorator Pattern
The Decorator Pattern is a structural design pattern that allows developers to add new behavior to an object dynamically without changing its original class. It works by wrapping an object inside another object that provides additional functionality while keeping the same interface.
This pattern is useful when an application needs to extend behavior in a flexible way without creating many subclasses. It supports clean object-oriented design by allowing features such as logging, caching, validation, formatting, authorization, compression, encryption, or notification behavior to be added around an existing object.
Introduction
In the previous article of this Design Patterns series, we discussed the Adapter Pattern. Adapter is used when two classes or systems have incompatible interfaces and need a bridge to work together.
The Decorator Pattern is also a structural design pattern, but it solves a different problem. Instead of changing an interface, Decorator keeps the same interface and adds extra behavior around the original object.
In real software projects, developers often need to add features to existing classes without modifying their source code. For example, a service may need logging, caching, validation, or access control. Adding all of these features directly into the original class can make it large and difficult to maintain. Decorator Pattern helps keep the original class focused while allowing extra behavior to be added separately.
What Is the Decorator Pattern?
The Decorator Pattern is a design pattern that wraps an object with another object to add new behavior before, after, or around the original behavior. The wrapper object is called a decorator.
The important idea is that the decorator implements the same interface as the object it wraps. This means the client code can use the original object or the decorated object in the same way.
For example, if an application has a notification sender with a send method, a logging decorator can also implement the same send method. Inside that method, it can log the message and then call the original sender.
Real-Life Example of Decorator
A real-life example of decoration is adding features to a basic coffee. A simple coffee can be decorated with milk, sugar, chocolate, or cream. The base drink remains coffee, but each decoration adds something extra.
In software, the same idea applies. A basic object can be wrapped with one or more decorators that add new behavior while still behaving like the original object from the outside.
This makes it possible to combine features without creating a separate subclass for every possible combination.
Why the Decorator Pattern Is Important
The Decorator Pattern is important because it helps developers extend behavior without modifying existing code. This supports the Open Closed Principle, which says that software should be open for extension but closed for modification.
Instead of changing the original class every time a new feature is needed, developers can create a new decorator. This reduces the risk of breaking existing behavior and makes the code easier to test.
Decorator is also useful when behavior needs to be combined dynamically. For example, a file storage service may be decorated with logging, caching, and encryption depending on configuration.
Problem Without Decorator Pattern
Imagine an application that sends notifications. At first, the notification sender only sends messages. Later, the application needs logging. Then it needs validation. Then it needs retry logic. Then it needs performance monitoring.
Without Decorator Pattern, developers may add all of these responsibilities directly into the original notification class:
class EmailNotificationSender
{
public function send(string $recipient, string $message): bool
{
// Validate message
// Log sending attempt
// Send email
// Retry on failure
// Record performance metrics
return true;
}
}This class now has too many responsibilities. It is not only sending emails. It is also validating, logging, retrying, and monitoring.
Decorator Pattern solves this by separating each additional behavior into its own wrapper class.
Basic Decorator Pattern Structure
The Decorator Pattern usually includes the following parts:
Component interface: Defines the common behavior used by both the original object and decorators.
Concrete component: The original object that provides the core behavior.
Base decorator: A wrapper that stores a component object and implements the same interface.
Concrete decorators: Classes that add specific behavior before or after calling the wrapped object.
This structure allows decorators to be stacked and combined in different ways.
Basic Decorator Example in PHP
The following example shows a simple Decorator Pattern implementation for notifications:
interface NotificationSender
{
public function send(string $recipient, string $message): bool;
}
class EmailNotificationSender implements NotificationSender
{
public function send(string $recipient, string $message): bool
{
// Send email notification
return true;
}
}
abstract class NotificationSenderDecorator implements NotificationSender
{
public function __construct(
protected NotificationSender $sender
) {
}
}
class LoggingNotificationDecorator extends NotificationSenderDecorator
{
public function send(string $recipient, string $message): bool
{
echo 'Logging notification before sending.' . PHP_EOL;
return $this->sender->send($recipient, $message);
}
}In this example, LoggingNotificationDecorator wraps another NotificationSender and adds logging behavior before sending the notification.
Using the Decorator
The decorated object can be used through the same interface:
$sender = new EmailNotificationSender();
$senderWithLogging = new LoggingNotificationDecorator($sender);
$senderWithLogging->send('user@example.com', 'Welcome to our platform.');The client code calls send in the same way. It does not need to know whether the object is the original sender or a decorated sender.
This is one of the main strengths of the Decorator Pattern. It adds behavior without changing how the object is used.
Adding Multiple Decorators
One decorator can wrap another decorator. This allows multiple behaviors to be combined dynamically.
For example, we can add validation and logging decorators around the same notification sender:
class ValidationNotificationDecorator extends NotificationSenderDecorator
{
public function send(string $recipient, string $message): bool
{
if (empty($recipient)) {
throw new InvalidArgumentException('Recipient is required.');
}
if (empty($message)) {
throw new InvalidArgumentException('Message is required.');
}
return $this->sender->send($recipient, $message);
}
}
class RetryNotificationDecorator extends NotificationSenderDecorator
{
public function send(string $recipient, string $message): bool
{
try {
return $this->sender->send($recipient, $message);
} catch (Throwable $exception) {
return $this->sender->send($recipient, $message);
}
}
}Now the object can be decorated with multiple layers:
$sender = new EmailNotificationSender();
$sender = new ValidationNotificationDecorator($sender);
$sender = new LoggingNotificationDecorator($sender);
$sender = new RetryNotificationDecorator($sender);
$sender->send('user@example.com', 'Your order has been shipped.');Each decorator adds a specific responsibility while keeping the same NotificationSender interface.
Decorator Pattern and Composition
The Decorator Pattern is based on composition. Instead of extending the original class through inheritance, the decorator contains another object and delegates work to it.
This is important because composition is often more flexible than inheritance. With inheritance, adding many combinations of behavior can lead to many subclasses. With decorators, behavior can be combined dynamically by wrapping objects.
This supports the common object-oriented principle: prefer composition over inheritance.
Decorator Pattern vs Inheritance
Inheritance can add behavior by creating child classes, but it becomes difficult when many feature combinations are needed.
For example, imagine a notification sender with optional logging, caching, validation, retrying, encryption, and monitoring. Using inheritance may require many classes such as LoggedEmailSender, CachedEmailSender, ValidatedEmailSender, LoggedCachedEmailSender, and many more combinations.
Decorator avoids this problem by allowing each feature to be placed in a separate wrapper that can be combined as needed.
Real-World Example: Caching Decorator
Caching is a common use case for the Decorator Pattern. A service may fetch data from a slow API or database. A caching decorator can store the result and return cached data on future calls.
interface ProductProvider
{
public function find(int $id): array;
}
class ApiProductProvider implements ProductProvider
{
public function find(int $id): array
{
// Fetch product from external API
return [
'id' => $id,
'name' => 'Laptop',
];
}
}
class CachedProductProvider implements ProductProvider
{
private array $cache = [];
public function __construct(
private ProductProvider $provider
) {
}
public function find(int $id): array
{
if (!isset($this->cache[$id])) {
$this->cache[$id] = $this->provider->find($id);
}
return $this->cache[$id];
}
}The CachedProductProvider decorates the original product provider and adds caching behavior without modifying ApiProductProvider.
Real-World Example: Logging Decorator
Logging decorators are useful when developers want to record operations without adding logging code directly into business classes.
class LoggedProductProvider implements ProductProvider
{
public function __construct(
private ProductProvider $provider,
private LoggerInterface $logger
) {
}
public function find(int $id): array
{
$this->logger->info('Finding product', ['id' => $id]);
$product = $this->provider->find($id);
$this->logger->info('Product found', ['id' => $id]);
return $product;
}
}This keeps the original provider focused on fetching products while the decorator handles logging.
Real-World Example: Authorization Decorator
An authorization decorator can check permissions before allowing an operation to continue.
interface ReportExporter
{
public function export(array $data): string;
}
class PdfReportExporter implements ReportExporter
{
public function export(array $data): string
{
return 'PDF report exported';
}
}
class AuthorizedReportExporter implements ReportExporter
{
public function __construct(
private ReportExporter $exporter,
private User $user
) {
}
public function export(array $data): string
{
if (!$this->user->can('export_reports')) {
throw new RuntimeException('User is not allowed to export reports.');
}
return $this->exporter->export($data);
}
}The authorization logic is separated from the PDF export logic. This improves separation of responsibilities.
Real-World Example: Compression and Encryption
Decorator Pattern is also useful when processing files or data streams. A basic file writer can be decorated with compression and encryption.
interface FileWriter
{
public function write(string $content): string;
}
class PlainFileWriter implements FileWriter
{
public function write(string $content): string
{
return $content;
}
}
class CompressionFileWriter implements FileWriter
{
public function __construct(
private FileWriter $writer
) {
}
public function write(string $content): string
{
$compressed = gzcompress($content);
return $this->writer->write($compressed);
}
}
class EncryptionFileWriter implements FileWriter
{
public function __construct(
private FileWriter $writer
) {
}
public function write(string $content): string
{
$encrypted = base64_encode($content);
return $this->writer->write($encrypted);
}
}The application can combine these decorators depending on the required behavior.
Decorator Pattern in Laravel
In Laravel applications, Decorator Pattern can be used with services, repositories, API clients, payment gateways, notification senders, and cache layers.
For example, a Laravel application may have a ProductRepository interface. The main repository fetches products from the database, while a caching decorator adds cache behavior around it.
$this->app->bind(ProductRepository::class, function ($app) {
$repository = new DatabaseProductRepository();
return new CachedProductRepository($repository, $app->make(CacheRepository::class));
});This allows the application to use ProductRepository normally while caching is added transparently.
Decorator Pattern in Symfony
Symfony has strong support for service decoration through its dependency injection container. Developers can decorate services to add behavior such as logging, caching, tracing, or security checks without modifying the original service.
This makes Decorator Pattern very practical in Symfony applications. The service container can automatically wrap one service with another decorator service.
This is useful in large applications where cross-cutting concerns should be added cleanly.
Decorator Pattern and Middleware
Middleware in web frameworks is conceptually related to the Decorator Pattern. Middleware wraps request handling and adds behavior before or after the next layer is executed.
For example, middleware can add authentication, logging, rate limiting, CORS handling, or request transformation. Each middleware layer wraps the next operation and contributes additional behavior.
Although middleware is not always implemented exactly as the classic Decorator Pattern, the idea of wrapping behavior is very similar.
Decorator Pattern and Cross-Cutting Concerns
Cross-cutting concerns are behaviors that affect many parts of an application. Examples include logging, caching, authorization, validation, monitoring, retry logic, and error handling.
If these concerns are added directly into every service, the code becomes duplicated and harder to maintain. Decorator Pattern allows these concerns to be separated into reusable wrappers.
This improves clean architecture and keeps business classes focused on their main responsibility.
Decorator Pattern vs Adapter Pattern
Decorator Pattern and Adapter Pattern both use wrappers, but they have different goals.
The Adapter Pattern changes the interface of an object so it can work with code that expects a different interface. It is mainly about compatibility.
The Decorator Pattern keeps the same interface but adds new behavior around the object. It is mainly about extension.
In short, Adapter changes how an object is accessed, while Decorator adds behavior without changing the interface.
Decorator Pattern vs Facade Pattern
The Facade Pattern provides a simple interface to a complex subsystem. It hides complexity behind a simpler API.
The Decorator Pattern adds behavior to an object while keeping the same interface. It does not necessarily simplify a subsystem. Instead, it extends an existing object dynamically.
In short, Facade simplifies access, while Decorator enhances behavior.
Decorator Pattern vs Proxy Pattern
The Proxy Pattern controls access to another object. It may add lazy loading, access control, remote communication, or caching. The Decorator Pattern adds behavior while preserving the same interface.
These patterns can look similar because both wrap another object. The difference is mostly in intent. Proxy controls access, while Decorator adds responsibilities.
Benefits of Decorator Pattern
The Decorator Pattern provides many benefits in object-oriented software design.
Main benefits include:
Adds behavior without modifying the original class.
Supports the Open Closed Principle.
Reduces the need for many subclasses.
Allows behavior to be combined dynamically.
Improves separation of responsibilities.
Works well with interfaces and dependency injection.
Helps isolate cross-cutting concerns.
Makes features such as logging and caching reusable.
These benefits make Decorator Pattern especially useful in medium and large applications.
Drawbacks of Decorator Pattern
The Decorator Pattern can also have drawbacks. It may create many small classes, which can make the project harder to navigate if the structure is not organized well.
Another drawback is that debugging can become more difficult when an object is wrapped by many layers. Developers may need to trace through several decorators to understand the full behavior.
Decorator order can also matter. For example, logging before caching may produce different results than caching before logging. Developers should be careful when stacking decorators.
When to Use Decorator Pattern
Use the Decorator Pattern when you need to add behavior to objects without modifying their original classes.
Decorator Pattern is useful when:
You want to add logging, caching, validation, or authorization around a service.
You need to combine multiple optional behaviors dynamically.
You want to avoid creating many subclasses.
You want to keep the original class focused and clean.
You need to follow the same interface while adding extra behavior.
You want to separate cross-cutting concerns from business logic.
If these conditions exist, Decorator Pattern can be a strong design choice.
When Not to Use Decorator Pattern
Do not use Decorator Pattern when the behavior is simple and unlikely to change. If adding a feature directly to a class is clear and does not break responsibility boundaries, a decorator may be unnecessary.
Avoid Decorator Pattern when:
The object does not need dynamic behavior extension.
The pattern adds too many classes without real value.
The same behavior can be handled more simply by configuration.
The order of decorators becomes confusing.
The feature belongs naturally inside the original class.
Design patterns should make the code cleaner, not more complicated.
Common Mistakes with Decorator Pattern
One common mistake is changing the interface in a decorator. A decorator should usually keep the same interface as the object it wraps. If it changes the interface, it may actually be acting like an adapter.
Another mistake is putting unrelated business logic inside decorators. Decorators should add specific extra behavior, not become large service classes.
A third mistake is stacking decorators without controlling their order. Some decorators must run before others, and the order should be clear.
A fourth mistake is creating decorators for every small feature. If the feature is simple and belongs inside the class, a decorator may be unnecessary.
Best Practices for Decorator Pattern
To use the Decorator Pattern effectively, developers should keep decorators small, focused, and consistent.
Useful best practices include:
Use a clear interface shared by the original object and decorators.
Keep each decorator focused on one responsibility.
Use dependency injection to wrap services cleanly.
Use meaningful names such as CachedProductProvider or LoggedPaymentGateway.
Keep decorator order clear when multiple decorators are used.
Avoid changing the public interface in decorators.
Use decorators for cross-cutting concerns such as logging, caching, and authorization.
Do not overuse decorators for simple problems.
These practices help keep the Decorator Pattern maintainable and practical.
Practical Checklist Before Using Decorator Pattern
Before using the Decorator Pattern, developers can ask these questions:
Do I need to add behavior without changing the original class?
Can the new behavior be separated into a focused wrapper?
Should the decorated object keep the same interface?
Will this avoid too many subclasses?
Will this make cross-cutting concerns cleaner?
Can decorators be combined safely?
Is the added complexity worth the flexibility?
If the answer is yes to several of these questions, Decorator Pattern may be a good design choice.
Conclusion
The Decorator Pattern is a structural design pattern that allows developers to add behavior to objects dynamically without modifying their original classes. It works by wrapping an object with another object that implements the same interface and adds extra functionality.
Decorator Pattern is useful for logging, caching, validation, authorization, retry logic, monitoring, compression, encryption, and other cross-cutting concerns. It supports clean code, composition, dependency injection, and the Open Closed Principle.
However, Decorator should be used carefully. Too many decorators can make debugging and object flow harder to understand. When applied correctly, the Decorator Pattern is a powerful tool for building flexible, maintainable, and extensible object-oriented software.

