Design Patterns
Design Patterns
Ngane Emmanuel
2024-10-23
Design patterns are tried-and-tested solutions to common software design problems. They provide a reusable
and standard approach to solving recurring issues in software development. Think of them like a recipe—by
following the pattern, developers can efficiently solve problems with well-established practices.
Design patterns originated from the book “Design Patterns: Elements of Reusable Object-Oriented Software”
(1994) by the “Gang of Four” (Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides). This book
categorized patterns into different groups, laying the foundation for modern software architecture practices.
Design patterns help developers write cleaner, more maintainable code. They promote best practices, reduce
technical debt, and enable efficient communication among developers by providing a shared vocabulary for
solutions.
Spring Boot simplifies the development of Java-based applications. However, with simplicity comes complex-
ity in scaling, maintaining, and managing dependencies. Design patterns in Spring Boot help: - Organize
code better (e.g., with Dependency Injection). - Enable reusable components (e.g., Singleton Beans). - Make
applications more robust and easier to maintain.
1
3. Creational Design Patterns in Spring Boot
The Singleton pattern ensures a class has only one instance, providing a global access point to that instance.
In Spring Boot, this concept aligns with how beans are managed—by default, Spring beans are Singleton
scoped.
@Component
public class AppConfig {
public AppConfig() {
System.out.println("Singleton Bean Created");
}
}
// Usage in a Controller
@RestController
public class HelloController {
@GetMapping("/hello")
public String sayHello() {
return "Hello from Singleton Bean!";
}
}
Think of a traffic signal controller. There is only one signal control system for an intersection that all
lights depend on—this is similar to a Singleton bean where one instance manages traffic (or configuration)
across the entire system.
When to Use
• When you need a single instance for centralized management (e.g., configuration settings).
• For stateful services where only one instance must exist.
2
3.2 Factory Pattern
The Factory pattern provides an interface for creating objects, allowing subclasses to decide which class to
instantiate. It’s useful when the creation logic is complex or involves multiple options.
Variants include:
• Simple Factory
• Factory Method
• Abstract Factory
@Component("PayPal")
public class PayPalService implements PaymentService {
public void processPayment() {
System.out.println("Processing payment via PayPal.");
}
}
@Component("Stripe")
public class StripeService implements PaymentService {
public void processPayment() {
System.out.println("Processing payment via Stripe.");
}
}
// Factory Class
@Component
public class PaymentFactory {
@Autowired
private ApplicationContext context;
// Controller Usage
@RestController
public class PaymentController {
3
this.paymentFactory = paymentFactory;
}
@PostMapping("/pay/{service}")
public String processPayment(@PathVariable String service) {
PaymentService paymentService = paymentFactory.getPaymentService(service);
paymentService.processPayment();
return "Payment processed!";
}
}
Example Scenario
Think of online shopping platforms like Amazon. Depending on the selected payment method (PayPal,
Credit Card, or Stripe), the platform dynamically selects the appropriate service provider.
The Builder pattern helps construct complex objects step-by-step, ensuring readability and flexibility.
4
public User build() {
return new User(this);
}
}
}
// Usage
User user = new User.Builder()
.setUsername("johndoe")
.setEmail("[email protected]")
.setRole("Admin")
.build();
Think of constructing a pizza order. You add toppings, set the size, and choose the type of crust—one
step at a time. Similarly, the Builder pattern helps you build complex objects like user profiles or HTTP
requests.
The Adapter pattern allows incompatible interfaces to work together. In Spring Boot, this can be seen when
integrating third-party APIs into controllers.
@RestController
public class WeatherController {
@GetMapping("/weather/{city}")
public String getWeather(@PathVariable String city) {
// Adapter logic to convert external API response to internal format
return "Weather data for " + city;
}
}
5
Code Example: Logging and Caching with Proxies
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("Executing: " + joinPoint.getSignature().getName());
}
@Around("execution(* com.example.service.*.*(..))")
public Object cacheResult(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("Checking cache before execution...");
// Simulated caching logic
Object result = joinPoint.proceed();
System.out.println("Storing result in cache...");
return result;
}
}
Think of an airport check-in desk. Before passengers are granted boarding passes, staff check their
documents—this process acts like a proxy to ensure only authorized individuals proceed to boarding. Simi-
larly, a proxy limits or controls access to resources.
When to Use
The Decorator pattern allows behavior to be added to individual objects dynamically without altering the
behavior of other objects. It’s often used to enhance the functionality of objects.
@Component
public class EmailNotifier implements Notifier {
public void send(String message) {
6
System.out.println("Sending email: " + message);
}
}
@Component
public class SlackNotifierDecorator implements Notifier {
@Autowired
public SlackNotifierDecorator(EmailNotifier emailNotifier) {
this.wrapped = emailNotifier;
}
@Override
public void send(String message) {
wrapped.send(message);
System.out.println("Sending Slack notification: " + message);
}
}
// Usage in a Controller
@RestController
public class NotificationController {
@Autowired
public NotificationController(SlackNotifierDecorator notifier) {
this.notifier = notifier;
}
@PostMapping("/notify")
public String notify(@RequestBody String message) {
notifier.send(message);
return "Notification sent!";
}
}
Imagine sending notifications via both email and Slack. Instead of rewriting the notification logic, you
decorate the original EmailNotifier with additional functionality to send Slack messages, too.
7
5. Behavioral Design Patterns in Spring Boot
The Observer pattern allows multiple objects (observers) to react to changes in the state of another object
(subject). In Spring Boot, it is implemented using Application Events.
@Component
public class UserRegistrationListener {
@EventListener
public void onUserRegistration(UserRegisteredEvent event) {
System.out.println("User registered: " + event.getUser().getName());
}
}
@Autowired
public UserController(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
@PostMapping("/register")
public String registerUser(@RequestBody User user) {
publisher.publishEvent(new UserRegisteredEvent(user));
return "User registered!";
}
}
Imagine a social media platform where users receive notifications when a friend posts an update. The
Observer pattern ensures that all interested parties are notified when an event (like registration) occurs.
8
5.2 Template Method Pattern
The Template Method pattern defines the structure of an algorithm in a base class while allowing subclasses
to modify parts of the algorithm. Spring’s JdbcTemplate is a great example, as it provides a template for
database operations while leaving specific SQL details to be provided by the developer.
@Repository
public class UserRepository {
@Autowired
public UserRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
Think of a fast-food restaurant. The cooking process is standardized (template), but customers can
choose their toppings (custom behavior). Similarly, the JdbcTemplate standardizes database operations
while allowing customization.
The Command pattern encapsulates a request as an object, allowing you to parameterize clients with different
requests and support undoable operations.
@Component
public class LightOnCommand implements Command {
@Override
public void execute() {
System.out.println("Light turned on.");
}
}
@Component
public class LightOffCommand implements Command {
@Override
9
public void execute() {
System.out.println("Light turned off.");
}
}
@Autowired
public LightController(LightOnCommand onCommand, LightOffCommand offCommand) {
this.onCommand = onCommand;
this.offCommand = offCommand;
}
@PostMapping("/light/{action}")
public String controlLight(@PathVariable String action) {
if ("on".equals(action)) {
onCommand.execute();
} else {
offCommand.execute();
}
return "Light " + action + "!";
}
}
Think of a TV remote where each button press triggers a command. Using the Command pattern, you
can even implement undo/redo functionality—just like canceling a previous action.
6.1 Scalability
Design patterns such as the Singleton and Microservice architecture help manage shared resources
efficiently, allowing the system to scale horizontally. For instance, in a Spring Boot microservice environment,
breaking applications into small services increases scalability as each service can scale independently.
6.2 Maintainability
Patterns like Template Method and Factory promote reusable and consistent code structures. This makes
the code easy to maintain over time, as developers follow a standard practice for implementation.
Example: If all database operations use JdbcTemplate, adding or changing the underlying logic requires
minimal effort, making maintenance faster.
10
6.3 Performance Optimization
Patterns such as Proxy and Decorator can improve performance by implementing caching and lazy loading.
In Spring Boot, proxies can intercept expensive service calls and return cached results when applicable.
Example: Caching service responses through proxies reduces repeated database access, thereby improving
performance.
6.4 Security
The Proxy pattern is also useful for implementing security aspects, such as validating user credentials
before allowing access to critical resources. Singletons can enforce a single authentication mechanism
across the application.
Behavioral patterns like Observer and Command make applications more user-friendly by adding real-
time responsiveness and undo/redo functionality. This makes the application adaptable to changing user
needs.
7.1 Advantages
1. Reusability: Design patterns promote the reuse of solutions for common software challenges, reducing
development time.
2. Consistency: They ensure that developers follow a standard way of solving problems, resulting in
consistent and predictable code.
3. Extensibility: Patterns like Decorator allow you to extend functionality without altering existing
code.
4. Separation of Concerns: Architectural patterns like Microservice separate logic into independent
units, promoting modularity and ease of debugging.
5. Testability: Using patterns like Command makes it easier to test individual commands indepen-
dently.
7.2 Disadvantages
1. Overhead: Applying patterns where they aren’t needed can lead to unnecessary complexity.
2. Learning Curve: Understanding and correctly implementing patterns requires experience and knowl-
edge.
3. Over-engineering Risk: Patterns might introduce excessive abstractions if used incorrectly, compli-
cating code unnecessarily.
4. Performance Trade-offs: Some patterns, such as Observer, can introduce latency due to the
overhead of notifying multiple observers.
11
8. Practical Scenarios for Applying Design Patterns in Spring Boot
In an e-commerce platform, notifications are sent to users when new products arrive or when their order
status changes. The Observer pattern ensures that all relevant parties (customers, suppliers) receive
updates automatically.
A banking application follows specific steps for loan processing—like credit checks and document validation—
while allowing customization of specific logic. The Template Method pattern ensures that the overall
flow is consistent across different loan types.
In IoT environments, actions like turning lights on or off are triggered by specific commands. With the
Command pattern, you can implement undo/redo functionality, such as switching appliances back to
their previous state in case of an error.
In a social media app, the Proxy pattern is used to limit the rate of API calls, ensuring fair usage and
avoiding server overload. Proxies also manage authentication for accessing user profiles.
10. Conclusion
Design patterns are essential tools in software development, helping developers build scalable, maintainable,
and reliable applications. In the context of Spring Boot, patterns like Singleton, Proxy, Observer, and
Template Method are commonly applied to address various challenges such as performance optimization,
modularization, and user experience. While these patterns offer numerous advantages, it is crucial to use
them wisely to avoid over-complicating solutions. With proper application, design patterns not only improve
the quality of code but also contribute to the overall success of a project.
12
11. References
1. Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of
Reusable Object-Oriented Software. Addison-Wesley.
2. Craig Walls. (2016). Spring in Action, 5th Edition. Manning Publications.
3. Rod Johnson. (2014). Expert One-on-One J2EE Development without EJB. Wrox.
13