📘 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
- Always inject the delegate – don’t manually call
new
- Name your beans clearly – helps avoid conflicts
- Chain decorators carefully – each should depend on the previous
- Avoid too many layers – use only when needed
- 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
Post a Comment
Leave Comment