Decorator Pattern: Adding Features Without Changing the Core
By Misbahul Munir3 min read666 words

Decorator Pattern: Adding Features Without Changing the Core

Backend Development
design pattern
structural pattern

After exploring how to flexibly swap behaviors using the Strategy Pattern, we now venture into one of the most elegant ways to add functionality dynamically: the Decorator Pattern.

The Decorator Pattern lets you wrap an object with additional responsibilities, without modifying its structure. It adheres to the open/closed principle—open for extension, but closed for modification.


What Is the Decorator Pattern?

The Decorator Pattern involves a set of decorator classes that are used to wrap concrete components. Essentially, these wrappers add new behaviors or responsibilities to an existing object transparently without altering its interface. This is particularly useful when you want to extend functionalities in a modular way.


Real-World Analogy: Coffee Customization

Imagine visiting a coffee shop where you can customize your coffee order. Start with a basic coffee, and then add extras such as milk, sugar, or flavored syrups. Each extra can be seen as a decorator—enhancing your coffee experience without changing the core product.

You might begin with a simple coffee, but by layering ingredients, you end up with a personalized beverage. The shop doesn't need to create a separate class for every possible coffee type; it simply decorates a standard coffee object with the desired addons.


Real Backend Example: Enhancing HTTP Request Handlers

Consider a scenario in a backend system where you want to dynamically add behaviors to HTTP request handlers. For example, you might need to log requests or perform authentication checks without modifying the core handler code.

Step 1: Define the Base Component

Let's start by defining a simple request handler class:

class RequestHandler: def handle(self, request: dict) -> dict: # Basic request handling logic response = {"status": 200, "data": "Processed request"} return response

Step 2: Create the Decorator Base Class

The decorator base class should follow the same interface as the component it wraps:

class HandlerDecorator(RequestHandler): def __init__(self, handler: RequestHandler): self._handler = handler def handle(self, request: dict) -> dict: # Delegate the request to the wrapped handler return self._handler.handle(request)

Step 3: Implement Concrete Decorators

Now, we can create decorators to add logging and authentication functionalities:

class LoggingDecorator(HandlerDecorator): def handle(self, request: dict) -> dict: print(f"[LOG] Received request: {request}") response = self._handler.handle(request) print(f"[LOG] Sending response: {response}") return response class AuthenticationDecorator(HandlerDecorator): def handle(self, request: dict) -> dict: if not request.get("authenticated", False): return {"status": 401, "data": "Unauthorized"} return self._handler.handle(request)

Usage Example

Now, we can wrap our basic request handler with logging and authentication:

# main.py # Basic handler handler = RequestHandler() # Add authentication check auth_handler = AuthenticationDecorator(handler) # Add logging on top of authentication logged_auth_handler = LoggingDecorator(auth_handler) # Simulated requests unauthenticated_request = {"authenticated": False, "path": "/api/data"} authenticated_request = {"authenticated": True, "path": "/api/data"} print(logged_auth_handler.handle(unauthenticated_request)) # Output: {"status": 401, "data": "Unauthorized"} print(logged_auth_handler.handle(authenticated_request)) # Logs will show the request and response details, # followed by {"status": 200, "data": "Processed request"}

This layered approach demonstrates how you can dynamically “decorate” your request handler with additional features in a flexible and maintainable way.


When to Use the Decorator Pattern in Backend Systems

  • Logging and Monitoring: Attach logging decorators to capture request/response data.
  • Authentication and Authorization: Add security checks without altering core request logic.
  • Caching: Wrap handlers to add caching layers.
  • Transaction Management: Surround database operations with transaction decorators.

Gotchas

  • Complexity: Overuse of multiple decorators can lead to difficult-to-follow code if not managed properly.
  • Order Matters: The order in which decorators are applied is significant. Changing the order might alter behavior unexpectedly.
  • Debugging: Tracing the flow of decorated objects can be tricky during debugging.

Final Thoughts

The Decorator Pattern offers a powerful, flexible method to extend the behavior of your components dynamically. It’s especially useful in backend systems where middleware functionalities—like logging, security, caching, or transactions—need to be applied transparently.

By isolating additional responsibilities in decorators, you keep your core business logic clean and focused. This modular design is both maintainable and scalable, making it a favorite for modern backend development.