The Decorator Pattern in 3 Minutes
Let’s dive into the fascinating world of decorators.
Overview
The Decorator pattern is a structural design pattern in object-oriented programming that allows you to dynamically add behavior to an object without changing its class.
It involves creating a decorator class that wraps the original object and provides additional functionality by adding new methods or modifying existing ones. The decorator class implements the same interface as the original object, so the client code can use it transparently.
The Decorator pattern is useful when you need to add functionality to objects at runtime or when it’s not feasible to extend them using inheritance. It allows you to avoid creating a large number of subclasses to handle different combinations of behaviors, which can lead to code duplication and maintenance issues.
Instead, you can create small, focused decorator classes that add specific features to the base object. The Decorator pattern promotes the Open-Closed Principle, which states that classes should be open for extension but closed for modification
The Problem
Let’s consider we are using an interface to interact with the database for fetching account information based on username:
interface AccountsRepo {
Account username(String username);
}And we are using an implementation of this interface to send promotional emails to various users:
class NotificationService {
private final AccountsRepo accounts;
public NotificationService(AccountsRepo accounts) {
this.accounts = accounts;
}
void sendPromotionalEmail(String username) {
var account = accounts.username(username);
// ...
sendEmail(account.email());
}
}Now, let’s assume we would like to avoid the DB queries if possible: we know that the username must be at least five characters long, so we can avoid going to the database if the argument is not valid:
void sendPromotionalEmail(String username) {
if (username.length() <= 5) {
throw new IllegalArgumentException("the username should be at least 5 characters long, but %s has less then this".formatted(username));
}
var account = accounts.username(username);
// ...
sendEmail(account.email());
}Additionally, we can add some sort of caching for the account, to avoid retrieving it twice. For simplicity, we’ll implement something very simple, using a HashMap:
private final Map<String, Account> cachedAccounts = new HashMap<>();
void sendPromotionalEmail(String username) {
if (username.length() <= 5) {
throw new IllegalArgumentException("the username should be at least 5 characters long, but %s has less then this".formatted(username));
}
Account account;
if (!cachedAccounts.containsKey(username)) {
cachedAccounts.put(username, accounts.username(username));
}
account = cachedAccounts.get(username);
// ...
sendEmail(account.email());
}Let’s pause here for now. As we can see, this method has the potential to quickly become more complex. Additional features, which may not be directly related to the sendPromotionalEmail method can begin to accumulate, making it more difficult to read and comprehend. This can lead to obscured business logic, impeding code maintenance and future development efforts.
The Solution
The solution is to extract small bits of code from here and move them to a more suitable place, such as AccountsRepo. Unfortunately though, sometimes, this interface can be provided by a library or framework, or you simply wouldn't want to add this validation there: for these cases, we can apply the decorator pattern.
To decorate this, we should declare a new interface that wraps the original one and enriches the call with some additional logic: for instance, the validation:
class ValidAccountsRepo implements AccountsRepo {
// constructor
private final AccountsRepo actual;
@Override
public Account username(String username) {
if (username.length() <= 5) {
throw new IllegalArgumentException("the username should be at least 5 characters long, but %s has less then this".formatted(username));
}
return actual.username(username);
}
}Perfect, let’s do the same for the caching now:
class CachedAccountsRepo implements AccountsRepo {
// constructor
private final AccountsRepo actual;
private final Map<String, Account> cachedAccounts = new HashMap<>();
@Override
public Account username(String username) {
if (!cachedAccounts.containsKey(username)) {
cachedAccounts.put(username, actual.username(username));
}
return cachedAccounts.get(username);
}
}We could continue down this path indefinitely, as there are many possible use cases. For instance, we could set a timeout, initiate a read-only transaction, or add logging information to data queries. However, let’s keep the momentum going by adding one more decorator and proceeding with our solution:
@Slf4j
class VerboseAccountsRepo implements AccountsRepo {
// constructor
private final AccountsRepo actual;
@Override
public Account username(String username) {
log.info("retrieving the user: " + username);
var user = actual.username(username);
log.info("retrieved data: " + user);
return user;
}
}Finally, let’s use these decorators in our service by wrapping the actual class that talks to the database and creating a nested structure:
class NotificationService {
private final AccountsRepo accounts;
public NotificationService(AccountsRepo accounts) {
this.accounts = new VerboseAccountsRepo(
new CachedAccountsRepo(
new ValidAccountsRepo(
accounts
)
)
);
}
void sendPromotionalEmail(String username) {
var account = accounts.username(username);
// ...
sendEmail(account.email());
}
}Moreover, we can reuse this nested declaration in other parts of the application, but we can omit some of the decorators if needed. For Example, we can only discard the cached and verbose decorators for a component that receives many requests and the account data changes constantly.
Conclusion
In this short article, we’ve explored the decorator pattern. We’ve learned how to implement it and when to use it, and we’ve seen the flexibility it provides.
We’ve learned that declaring interfaces for the key components of our application allows us to create decorators and comply with the open-closed principle.
Thank You!
Thanks for reading the article and please let me know what you think! Any feedback is welcome.
If you want to read more about clean code, design, unit testing, object-oriented programming, functional programming, and many others, make sure to check out my other articles. Do you like the content? Consider following or subscribing to the email list.
Finally, if you consider becoming a Medium member and supporting my blog, here’s my referral.
Happy Coding!






