Decorator Pattern in a Spring Boot Project

📘 Premium Read: Access my best content on Medium member-only articles — deep dives into Java, Spring Boot, Microservices, backend architecture, interview preparation, career advice, and industry-standard best practices.

🎓 Top 15 Udemy Courses (80-90% Discount): My Udemy Courses - Ramesh Fadatare — All my Udemy courses are real-time and project oriented courses.

▶️ Subscribe to My YouTube Channel (176K+ subscribers): Java Guides on YouTube

▶️ For AI, ChatGPT, Web, Tech, and Generative AI, subscribe to another channel: Ramesh Fadatare on YouTube

In real-world Spring Boot applications, there are times when you want to add extra behavior to a service — like logging, validation, or caching — without changing the original code.

You could wrap the logic inside the same class, but that can make the class large and harder to maintain. If you’ve ever wanted to add functionality dynamically and cleanly, the Decorator Pattern is your friend.

In this article, we’ll cover:

  • What the Decorator Pattern is (in simple terms)
  • Where it's useful in backend development
  • A full working example: adding logging and validation to a notification service
  • How to implement it cleanly in a Spring Boot project
  • When and when not to use it

Let’s get started.


What Is the Decorator Pattern?

The Decorator Pattern is a structural pattern that allows you to wrap an object to add new behavior without modifying its original code.

It’s like adding layers — you start with the core, then add extra logic like validation, logging, or security.

This is useful when you want to:

  • Keep your core business logic clean
  • Add new behavior at runtime
  • Avoid copying and pasting logic across services

Real-World Example: Sending Notifications

Scenario:

You have a NotificationService interface that sends messages (email, SMS, push). Over time, you need to:

  • Add logging for all messages
  • Validate recipient format before sending

You don’t want to modify the original service every time. Instead, you'll wrap it using decorators.

Let’s build this step-by-step using Spring Boot.


✅ Step 1: Define the Common Interface

This is the contract both the base service and decorators will follow.

public interface NotificationService {
    void send(String to, String message);
}

✅ Step 2: Implement the Base Notification Service

This is your actual sending logic — email, SMS, etc.

import org.springframework.stereotype.Component;

@Component("baseNotificationService")
public class BasicNotificationService implements NotificationService {

    @Override
    public void send(String to, String message) {
        // Simulate sending
        System.out.println("Sending notification to " + to + ": " + message);
    }
}

Notice the @Component("baseNotificationService") — we’ll use this for injecting into decorators.


✅ Step 3: Create a Logging Decorator

This class wraps another NotificationService and adds logging before and after sending.

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

@Component("loggingNotificationService")
public class LoggingNotificationDecorator implements NotificationService {

    private final NotificationService delegate;

    public LoggingNotificationDecorator(@Qualifier("baseNotificationService") NotificationService delegate) {
        this.delegate = delegate;
    }

    @Override
    public void send(String to, String message) {
        System.out.println("[LOG] Preparing to send notification...");
        delegate.send(to, message);
        System.out.println("[LOG] Notification sent successfully.");
    }
}

This adds logs without changing the base service.


✅ Step 4: Add a Validation Decorator

This adds a check before sending to ensure to is a valid email or phone number.

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

@Component
public class ValidationNotificationDecorator implements NotificationService {

    private final NotificationService delegate;

    public ValidationNotificationDecorator(@Qualifier("loggingNotificationService") NotificationService delegate) {
        this.delegate = delegate;
    }

    @Override
    public void send(String to, String message) {
        if (to == null || to.isBlank()) {
            throw new IllegalArgumentException("Recipient cannot be empty");
        }
        // You can add more validations based on format
        delegate.send(to, message);
    }
}

Now we have two layers: validation → logging → actual sending.


✅ Step 5: Use the Final Decorated Service

In your business logic, use the outermost decorator (which wraps all others underneath).

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

@Service
public class NotificationManager {

    private final NotificationService notificationService;

    public NotificationManager(@Qualifier("validationNotificationDecorator") NotificationService notificationService) {
        this.notificationService = notificationService;
    }

    public void notifyUser(String to, String message) {
        notificationService.send(to, message);
    }
}

Make sure the final decorator bean (ValidationNotificationDecorator) is injected — it internally wraps the logging decorator, which wraps the base service.


✅ Step 6: Expose an Endpoint to Test

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/notify")
public class NotificationController {

    @Autowired
    private NotificationManager notificationManager;

    @PostMapping
    public ResponseEntity<String> sendNotification(@RequestParam String to,
                                                   @RequestParam String message) {
        notificationManager.notifyUser(to, message);
        return ResponseEntity.ok("Notification sent to: " + to);
    }
}

Test It

Send a request:

POST /api/notify?to=user@example.com&message=Welcome

Console output:

[LOG] Preparing to send notification...
Sending notification to user@example.com: Welcome
[LOG] Notification sent successfully.

If to is empty:

HTTP 400: Recipient cannot be empty

✅ You now have a clean service chain with layered behaviors.


✅ Why Use the Decorator Pattern in Spring Boot?

Benefit Explanation
Keeps services focused Each decorator does one thing (log, validate, etc.)
Adds behavior without changing existing code Open for extension, closed for modification
Improves testing Test decorators separately
Works naturally with Spring DI Easy to wire via constructor injection
Avoids duplication No need to repeat logging/validation in multiple services

🔁 Where Else Can You Use Decorators?

Use Case Example
Caching Add caching layer around database calls
Authorization Add role-based access before executing logic
Retry logic Retry failed calls to external APIs
Rate limiting Prevent abuse for specific endpoints
Auditing Record changes made to critical systems

✅ Best Practices

  1. Always inject the delegate – don’t manually call new
  2. Name your beans clearly – helps avoid conflicts
  3. Chain decorators carefully – each should depend on the previous
  4. Avoid too many layers – use only when needed
  5. Keep decorators single-purpose – logging, validation, retry should be separate

⚠️ When Not to Use the Decorator Pattern

  • For small apps where added complexity isn’t justified
  • If the behavior is already handled by Spring AOP or filters
  • When the decoration needs runtime decisions (consider Strategy or Proxy)

Use the Decorator Pattern when you want reusable, layered enhancements — not when a simple interceptor will do the job.


Summary

Step What We Did
1 Defined a NotificationService interface
2 Created a base implementation to send messages
3 Built a logging decorator to add logging
4 Added a validation decorator for input checking
5 Used the outermost decorator in our service
6 Tested via a controller endpoint

The Decorator Pattern helps you build features step-by-step without creating huge classes or repeating code.


🏁 Final Thoughts

Spring Boot makes it easy to follow clean design patterns like Decorator — thanks to its dependency injection system. With a bit of planning, you can create flexible, testable, and maintainable services by wrapping them with decorators, instead of stuffing everything into one big class.

If you're layering things like logging, validation, caching, or retry — try implementing them as decorators. It keeps your code focused and easier to test.

Clean code is all about separation of concerns — and the Decorator Pattern delivers exactly that.

Comments

Spring Boot 3 Paid Course Published for Free
on my Java Guides YouTube Channel

Subscribe to my YouTube Channel (165K+ subscribers):
Java Guides Channel

Top 10 My Udemy Courses with Huge Discount:
Udemy Courses - Ramesh Fadatare