Sitemap

Why Adding Features Shouldn’t Break Your Code: Meet the Decorator Pattern

17 min readApr 22, 2025

We explored how to flexibly route and handle requests using the Chain of Responsibility pattern in our previous article Stop Hardcoding Logic: Use the Chain of Responsibility Instead. That pattern tackled the challenge of decoupling decision logic by passing a request through a chain of handlers. Now we turn to a different kind of extensibility challenge: how to add new behaviors to objects without modifying their existing code. Enter the Decorator design pattern. Decorator offers a clean way to extend an object’s functionality at runtime by wrapping it with new “layers” of behavior. Unlike Chain of Responsibility (which deals with who handles a request), the Decorator pattern deals with how to augment the behavior of a single object. This article will delve into the Decorator pattern and show how it enables flexible design through composition, with relatable examples in Dart and Flutter.

Decorator Design Pattern

What Is the Decorator Pattern?

The Decorator pattern (also known as the Wrapper pattern) is a structural design pattern that allows you to attach additional responsibilities to an object dynamically — that is, add new behavior to an individual object without altering its code or affecting other objects of the same class. The intent is: “Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.”. In simpler terms, instead of changing an object’s internals (its “guts”), you change its “skin” by wrapping it with another object that adds new behavior before or after delegating to the original object. The decorator implements the same interface as the original object and contains a reference to it, so from the outside it behaves like the original, just with extra capabilities.

Decorator Design Pattern (UML Diagrams) — Software Ideas Modeler

Think of a real-world analogy: coffee decorations. Imagine you have a basic cup of coffee. If you want to add milk, sugar, or whipped cream, you don’t create a brand new “MilkCoffee” or “WhippedSugarCoffee” subclass for every combination. Instead, you start with the plain coffee and decorate it by adding ingredients one by one. The coffee plus milk is still essentially a coffee (it can be served wherever a coffee is needed), but now it has an extra feature (milk flavor). You could then decorate that with sugar, and so on. Each addition wraps the coffee with a new behavior (changing its taste or cost) without altering the coffee class itself. In software, the Decorator pattern works the same way: we take an object and wrap it in one or more decorator objects that add features while preserving the original interface.

The Decorator pattern lets you extend an object’s behavior at runtime by wrapping it in helper objects called decorators. Each decorator is transparent to the client — the client sees the same interface — but internally, the decorator handles the request and then delegates to the original object (or the next decorator in line), adding something extra along the way. This approach of wrapping objects is an example of composition over inheritance, which we’ll discuss more soon.

Decorator vs. Inheritance

You might wonder, why not just use inheritance (subclasses) to extend behavior? Inheritance is a fundamental way to reuse and extend code, but it has limitations when it comes to flexibility. When you use inheritance, you’re adding behavior at compile time — the subclass has the extra logic for all its instances, and you can’t easily remove or vary that behavior per object at runtime. Also, if you foresee many possible combinations of features, using only inheritance can lead to an explosion of subclasses. For example, imagine a GUI toolkit where a TextBox can be scrollable and/or have a border. Using inheritance alone, you might create BorderedTextBox, ScrollableTextBox, and then BorderedScrollableTextBox… and if you add another feature like transparency, you’d multiply the combinations further. This quickly becomes unmanageable.

Decorator vs Inheritance — key differences:

  • Composition over Inheritance: The Decorator pattern prefers composition (wrapping objects) over subclassing. Instead of creating many subclasses for every feature combination, you create a core class and multiple decorator classes that can be applied as needed. This avoids the rigidity of a deep inheritance hierarchy and the combinatorial explosion of classes. Preventing a “subclass explosion” of feature-laden classes is a major motivation for decorators.
  • Dynamic, Per-Object Extension: Inheritance extends behavior for all instances of a class (and is fixed at compile time), whereas decorators can be applied to individual objects at runtime. You can decide to wrap or unwrap an object with a decorator on the fly, giving you much more flexibility. If a class is marked final or otherwise not open to subclassing, decorators are essentially the only way to add behavior to its instances.
  • Multiple Independent Extensions: With decorators, you can mix and match multiple behaviors by stacking decorators. For instance, you could wrap an object with a logging decorator, then with a caching decorator, then maybe with a validation decorator — each adding its piece of functionality. With inheritance, a single subclass would have to encompass all those behaviors at once. Decorators let you pay-as-you-go, adding only what you need.
  • Reuse Across Hierarchies: A decorator class that implements an interface can wrap any implementation of that interface. This means a single decorator (say, a “LoggingDecorator”) could be used to add logging to various unrelated classes as long as they share the same interface or abstract parent. In contrast, with inheritance you would need to create a separate subclass for each base class you want to extend with logging.

In summary, use inheritance when behavior is relatively static and uniform across all instances, and use Decorator when behavior needs to be optional, configurable, or layered per object. Decorator supports the Open/Closed Principle by allowing classes to be open for extension but closed for modification — you can extend an object’s functionality without changing its code.

When to Use the Decorator Pattern

How do you know if the Decorator pattern is a good fit for your problem? Here are some typical scenarios and use cases:

Adding Cross-Cutting Behavior Transparently

Use Decorator when you want to add functionality like logging, caching, authentication, or validation to an object without tangling that code into the object’s core logic.

For example, wrapping a service object in a logging decorator can record method calls, and a caching decorator can store results, all without modifying the service’s code. The client interacting with the service doesn’t need to know that these extra behaviors are being applied. This keeps the primary code cleaner and adheres to the Open/Closed Principle (new behaviors added via new decorators, not by altering existing classes).

User Interface Components

The Decorator pattern is very common in UI frameworks. For instance, in Flutter, most visual enhancements are done by composing widgets. If you have a Text widget and you want to give it padding, a border, or a scroll capability, you wrap it with widgets like Padding, Container (with decoration), or SingleChildScrollView.

Flutter developers naturally use composition — which is essentially the decorator approach — to add UI features. The base widget doesn’t change; instead, it’s wrapped in one or more decorator widgets that provide the additional functionality. This approach works “most naturally in a Flutter application,” since Flutter’s design encourages building UI by nesting widgets.

Extending Behavior of Legacy or Sealed Classes

If you need to enhance objects of a class that you cannot easily modify or subclass (perhaps it’s part of a library, or marked final), decorators are a great solution. You can create a wrapper that implements the same interface and add the needed functionality. Use Decorator when it’s awkward or impossible to use inheritance to extend behavior (for example, when dealing with a class you can’t subclass).

Combining Multiple Behaviors Flexibly

When you anticipate that an object might need various combinations of features, decorators are ideal. They let you stack features in arbitrary combinations.

A classic example is Java’s I/O classes — you can wrap a plain file stream with a buffering decorator, then wrap that with a decompression decorator, then an encryption decorator, and so on, depending on what features you need. The code new EncryptionStream(new GZipStream(new FileStream(...))) would give you a file stream that compresses then encrypts data. Each decorator adds one piece of functionality. If you only needed encryption and not compression, you could just omit the GZip decorator. This configurable stacking of behaviors at runtime is precisely what Decorator was made for.

Avoiding Monolithic Classes

If you find a class is trying to handle many optional features via flags or conditionals, consider refactoring to use decorators.

For example, instead of a single ReportGenerator class with booleans like includeHeader, includeFooter, encryptOutput, etc., you could have a base ReportGenerator, and separate decorators for adding a header, adding a footer, encrypting the output, etc. Each decorator would handle one feature, and you could wrap the base generator in whichever decorators are needed for a particular scenario. This yields cleaner, more maintainable code, and each decorator adheres to single-responsibility (one focuses on header adding, one on encryption, etc.).

Use the Decorator pattern when you want the ability to add or remove behaviors at runtime by wrapping objects, especially when those behaviors are orthogonal (independent) features that might be toggled or combined in different ways. If subclassing would result in too much rigidity or too many classes, that’s a sign that decorators might be a better solution.

Benefits of the Decorator Pattern

The Decorator pattern offers several benefits for designing flexible, maintainable software:

Extensibility and Open/Closed Principle

Decorators enable you to extend an object’s behavior without modifying its class. This directly supports the Open/Closed Principle — classes are open for extension but closed for modification. You can introduce new functionality via new decorator classes, without changing existing, tested code. In a large system, this means you can add features with less risk of breaking things.

Flexible Behavior Stacking

Because decorators can be layered, you have great flexibility in combining behaviors. You can wrap an object with multiple decorators to stack multiple features at once. For example, you might add logging, then encryption, then compression, all by stacking decorators on a data stream. The order of stacking can be varied to suit your needs (although you must be mindful of order if the behaviors interact). This gives a combinatorial flexibility that inheritance can’t match — instead of fixed subclass combinations, you can dynamically mix decorators. This is more flexible than static inheritance.

Per-Object Customization

With decorators, you can pick which objects get which features. Perhaps you have ten objects of the same class in memory, and only one of them needs an extra behavior — you can wrap just that one in a decorator, leaving the others untouched. This is much more efficient than subclassing because you don’t create a new subclass for a special case; you simply decorate the instance that needs it. Other instances of the original class are not affected at all.

Upholds Single Responsibility Principle

By splitting optional behaviors into separate decorator classes, each class has a focused responsibility. The core component class remains simple, and each decorator adds one specific feature. This modularizes code nicely. For example, if logging is separated into a LoggingDecorator, the logging code isn’t intermixed with business logic code. This separation makes the system easier to maintain and reason about. If a certain feature needs to change, you can tweak or replace the corresponding decorator without touching the rest.

Runtime Flexibility

Decorators can be added or removed at runtime. You can decide dynamically based on configuration or user input which decorators to apply. For instance, you might let users configure “enable verbose logging” — under the hood, you could wrap certain objects with logging decorators only if the setting is on. This is much harder to do with inheritance, since you can’t switch a subclass at runtime without complex conditional logic. The Decorator pattern lets you essentially modify the behavior of an object on the fly by wrapping or unwrapping decorators.

Reusability and Composability

Decorators are reusable components. You can write a decorator once (say, a CompressionDecorator for a stream) and use it in many places in the codebase wherever that feature is needed. Also, you can compose different decorators together in various arrangements to get new behavior without writing new code. This “lego-like” assembly can simplify extending the system. It also encourages designing to interfaces (since decorators and components share an interface), which improves substitutability and testability.

In short, Decorator gives you a design that is extensible, flexible, and granular. It lets you start with a simple base and layer on complexity as needed, rather than baking all possibilities into one big class. This can lead to cleaner code and easier maintenance, as well as the ability to configure behavior without a rebuild (since it’s done at runtime).

Limitations and How to Handle Them

Like any pattern, Decorator isn’t a panacea. There are some downsides and challenges to be aware of:

Increased Complexity and Indirection

Every layer of decorator adds a level of indirection to your code. If you stack many decorators, it can become harder to follow the flow of execution. A simple method call on the surface may pass through a long nesting of decorators, and understanding what actually happens may require peeling through multiple wrapper classes. This can make debugging more difficult — for instance, when stepping through code, you’ll jump in and out of numerous small methods across different objects. If overused, the pattern can lead to a “Russian doll” effect where you have to open several wrappers to find where a certain behavior is coming from.

To handle this, it’s important to keep decoration chains reasonably short and well-documented. Good naming conventions or runtime inspection tools can help make the structure clearer. Some languages offer ways to simplify debugging (like seeing the call stack or wrapping structure easily), but in general, moderation is key.

Many Small Classes

Decorator tends to proliferate classes since each concrete decorator is its own class. If your design calls for a lot of little features, you might end up with dozens of decorator classes. This can clutter the codebase and increase maintenance overhead.

One mitigation is to carefully evaluate if certain combinations of features are always used together — if so, you might implement a single decorator that adds both together, to reduce the total number.

Another approach is to use delegation in a more dynamic way (for example, using a single decorator class that takes a strategy or function for the extra behavior, if your language supports passing behaviors).

However, these approaches trade off pure decoupling for practicality. In modern languages with functional features, sometimes aspect-oriented programming or lambda wrappers can achieve similar outcomes without as many classes, but those are beyond the scope here. The main point is, be judicious — create decorators for truly distinct concerns and avoid creating a class for every tiny variation if it’s not necessary.

Ordering Issues

When you stack multiple decorators, the order in which they are applied matters in many cases. For example, if you have an encryption decorator and a compression decorator on a data stream, compressing then encrypting yields a different result than encrypting then compressing. If the order is wrong, you might get unintended behavior (e.g., you can’t compress effectively if data is encrypted).

Managing the correct order can add cognitive load. The onus is on the developer to apply decorators in the proper sequence.

A good practice is to clearly document any ordering requirements and maybe provide convenience functions to build the decorators in the right order. If certain orders don’t make sense, you’ll want to avoid those combinations or handle them internally (perhaps by making one decorator aware of another, though that complicates the design).

Object Identity and Removal

Since decorators wrap the original object, from the outside the wrapped object is not the same instance as the core component. If client code is expecting to work with the original object reference or do identity checks (== or equals on object identity), things can get tricky.

Generally, this is not a big issue because clients are meant to use the interface and not depend on the specific implementation. However, removing a specific decorator at runtime can be cumbersome — you’d need to traverse the chain of wrappers to find it. The Decorator pattern doesn’t have a built-in mechanism to remove a layer easily. Handling this might require your decorators to expose the wrapped object or provide a way to skip them, which can break the transparent interface.

In practice, if you need to frequently add and remove layers, another approach might be needed (or you manage the lifecycle carefully such that you discard the whole wrapped stack and rebuild it as needed).

Debugging and Testing Challenges

Testing a decorator in isolation is straightforward (since it typically delegates to a stub component), but testing an object with multiple decorators can be tricky if you need to ensure all layers worked together correctly. You might have to either test the end-to-end behavior (which is fine) or allow hooks for introspection (not typical). Logging or debugging output can sometimes be confusing if each decorator adds logs; you might see interwoven messages from multiple layers.

A strategy to handle this is to use consistent and clear log messages (prefix with the decorator name, for instance) so you can trace the sequence.

Potential Overuse

Because decorators are easy to add, there’s a temptation to wrap everything in sight “just in case.” Overusing them can lead to needless complexity. Always ask if a simpler solution would suffice (maybe a simple method parameter or a configuration flag, if the behavior is truly trivial). Use decorators when there is a clear need for extension variability or to separate concerns that truly warrant separate classes. If you find yourself layering more than a few decorators regularly, consider whether the design could be simplified or whether those concerns should be merged.

How to handle these limitations

The best approach is awareness and restraint. Use decorators for clear wins in flexibility and maintainability (adding significant behaviors, avoiding subclass explosion, etc.), but don’t wrap objects needlessly. Keep the number of layers minimal and the relationships obvious.

Good documentation or even visualization (perhaps printing the chain of decorators in a debug mode) can help during troubleshooting. When the structure gets too complex, reconsider if the design can be refactored. For instance, maybe using the Chain of Responsibility or Strategy pattern if that fits better for certain aspects, or combining certain decorators.

In practice, many frameworks that heavily use decoration (like UI frameworks) provide tooling to inspect the widget or component tree, which is essentially a decorator stack, so developers can debug layout issues. Similarly, if you implement decorators in your code, you could provide diagnostic methods to list the active decorators around an object. While that breaks the pure “transparent interface” somewhat, it might be useful for development and turned off in production.

Dart Code Example: Decorating a Data Source with Logging and Transformation

Let’s implement a scenario where we have a data source that provides data (say, reading from a file or an API). We want to add extra features: logging whenever data is fetched, and optionally transforming the data (for example, formatting or converting it). We’ll use the Decorator pattern to add these features so that the core data source doesn’t need to change.

// Component interface
abstract class DataSource {
String fetchData();
}

// Concrete component (base functionality)
class FileDataSource implements DataSource {
@override
String fetchData() {
// Imagine this reads from a file; we'll simulate with a static string
return "Hello, World!";
}
}

// Base Decorator class
class DataSourceDecorator implements DataSource {
final DataSource _wrappee;
DataSourceDecorator(this._wrappee);

@override
String fetchData() {
// Just delegate to the wrapped data source by default
return _wrappee.fetchData();
}
}

// Concrete Decorator: Logging
class LoggingDecorator extends DataSourceDecorator {
LoggingDecorator(DataSource wrappee) : super(wrappee);

@override
String fetchData() {
print("[Logging] About to fetch data...");
String data = super.fetchData(); // get data from wrapped component
print("[Logging] Fetched data: $data");
return data;
}
}

// Concrete Decorator: Transformation (e.g., convert to uppercase)
class UppercaseDecorator extends DataSourceDecorator {
UppercaseDecorator(DataSource wrappee) : super(wrappee);

@override
String fetchData() {
String data = super.fetchData();
// Transform the data (to uppercase in this example)
return data.toUpperCase();
}
}

Now, let’s see how we can use these:

void main() {
DataSource rawSource = FileDataSource();

// Use the raw source directly
print("Raw output: ${rawSource.fetchData()}\n");
// Raw output: Hello, World!

// Decorate the source with logging
DataSource loggingSource = LoggingDecorator(rawSource);
print("Logged output: ${loggingSource.fetchData()}\n");
// This will print log messages to console and then the data.
// [Logging] About to fetch data...
// [Logging] Fetched data: Hello, World!
// Logged output: Hello, World!

// Now decorate with transformation (uppercase) *on top of* logging
DataSource decoratedSource = UppercaseDecorator(loggingSource);
print("Transformed output: ${decoratedSource.fetchData()}");
// [Logging] About to fetch data...
// [Logging] Fetched data: Hello, World!
// Transformed output: HELLO, WORLD!
}

In this code, FileDataSource is our core provider (it could have been something like reading from disk; here it just returns a constant string for simplicity). We then have a LoggingDecorator that prints messages before and after fetching the data, and an UppercaseDecorator that post-processes the data by uppercasing it. Both decorators inherit from DataSourceDecorator which itself implements DataSource and holds a _wrappee object.

When we wrap the raw source with LoggingDecorator, any call to fetchData() on the logging wrapper will print the logs and then delegate to the file source. The file source returns "Hello, World!", which the logging decorator then prints out and returns. Next, we wrap the loggingSource with UppercaseDecorator. Now a call to fetchData() on this decoratedSource will go through UppercaseDecorator (which calls its wrapped object’s fetchData(), i.e. the loggingSource, then uppercases the result). The loggingSource in turn calls the file source and logs around it. The end result of decoratedSource.fetchData() is the original string uppercased, and along the way the logging messages are printed.

A few things to note:

  • We could have applied the uppercase decorator below the logging decorator (i.e., first uppercase then log). If we did loggingDecorator( uppercaseDecorator(fileSource) ), the logging would see and print the already uppercased data. The order of wrapping changed the observed behavior slightly (whether logging sees the transformed data or original). In this case it probably doesn’t matter, but it could in other scenarios. This shows the importance of ordering and understanding how each decorator works.
  • We can easily add more decorators: for example, a CachingDecorator that caches the result of fetchData() so that subsequent calls don’t hit the file again. If we had such a decorator, we could wrap it as needed. The DataSourceDecorator base makes it trivial to create new decorators – just override fetchData() and do something before or after calling super.fetchData().
  • From the outside, whether we have a raw FileDataSource, a LoggingDecorator(FileDataSource), an UppercaseDecorator(LoggingDecorator(FileDataSource)), etc., they all are DataSource as far as the code is concerned. We could pass any of those to a function that expects DataSource without it caring what combination of behaviors it has. This is the transparency of the pattern.

This example is analogous to real-world cases like decorating a data repository with caching or access control, or decorating a service call with logging. It demonstrates how we can start with a simple object and incrementally enhance it by wrapping in decorators. Each decorator is simple and focused, and we can mix them as needed. If later we decided we don’t want uppercase conversion, we can just remove that wrapper; if we want to add another transformation, we can write a new decorator class and include it. The core FileDataSource remains unchanged throughout.

Summary and Final Thoughts

When you want to extend behavior without cluttering your core classes, the Decorator pattern provides a clean path forward. It addresses the scenario where you want to add behavior to objects in a piecemeal way, avoiding rigid inheritance hierarchies or bloated classes that try to handle every feature. By wrapping objects with decorator layers, you adhere to principles like Open/Closed (extend without modifying) and Single Responsibility (each decorator has one job), while keeping your code flexible.

If you recall our previous discussion on the Chain of Responsibility pattern, you might notice a common theme: both patterns help us avoid hardcoding logic directly in classes and instead assemble behavior more flexibly. Chain of Responsibility achieves this by passing work through a chain of handlers, while Decorator achieves it by layering enhancements on an object. Both rely on object composition and can even look similar in structure, but they solve different problems. Where Chain of Responsibility was about multiple objects competing or collaborating to handle a request, Decorator is about one object augmented by multiple helpers to fulfill a request. In both cases, we keep our code open for extension — whether adding new handlers or new decorators — without modifying the core logic.

In conclusion, the Decorator pattern is a valuable technique for any developer aiming to write clean, extensible code. It encourages us to think in terms of components and wrappers rather than ever-growing classes. By using Decorator alongside patterns like Chain of Responsibility, Strategy, and others, we can design systems that are both flexible and easy to evolve. Keep the pattern in mind next time you find yourself adding one concern too many to a class — it might be a hint that a decorator (or two) could nicely refactor and organize that functionality.

If this article helped clarify the Decorator pattern for you, let me know with a few claps! Feel free to leave a comment — I read every one. And if you think a friend or teammate could use a refresher, don’t hesitate to share it. Want more? Hit that follow button so you don’t miss upcoming posts on software design and clean architecture.

Happy decorating! 🧑‍🎨

--

--

Maxim Gorin
Maxim Gorin

Written by Maxim Gorin

Team lead in mobile development with a passion for Fintech and Flutter. Sharing insights and stories from the tech and dev world on this blog.

Responses (3)