Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Behavioral Design Patterns for Flexible Architectures

Introduction

In our last lesson, we explored creational and structural design patterns, which provide frameworks for how objects are created and composed. We now turn our attention from the static structure of a system to its dynamic behavior. This lesson focuses on Behavioral Design Patterns, which are concerned with how objects interact, communicate, and delegate responsibilities to create flexible and loosely coupled systems.

Specifically, we will cover the Strategy and Observer patterns. You will learn how to use the Strategy pattern to make algorithms interchangeable at runtime, eliminating rigid conditional logic. Then, you'll see how the Observer pattern forms the foundation of event-driven architectures by allowing objects to subscribe to and react to state changes without being tightly coupled. For a developer with your Java and Spring Boot experience, mastering these patterns is a crucial step toward designing sophisticated, maintainable, and scalable applications.

1. The Strategy Pattern: Encapsulating Algorithms

Imagine a service in your application that needs to perform a task which can be done in multiple ways—for example, sorting a list of items based on different criteria (price, rating, relevance) or processing a payment through various gateways (Credit Card, PayPal, Bank Transfer). A common but brittle approach is to use a large if-else or switch statement to select the correct behavior. This violates the Open/Closed Principle because adding a new method requires modifying the existing class, making it harder to maintain and test.

The Strategy pattern solves this by defining a family of algorithms, encapsulating each one in its own class, and making them interchangeable. The client code, known as the "Context," works with these algorithms through a common interface, oblivious to the specific implementation details.

The following video provides an excellent walkthrough of this exact problem and shows how to refactor a cluttered PaymentService into an elegant solution using the Strategy pattern.

The Strategy Pattern Explained and Implemented in Java | Behavioral Design Patterns | Geekific

This video from Geekific, titled "The Strategy Pattern Explained," clearly illustrates the problem of monolithic conditional logic and the solution offered by the pattern.

Please watch the following segments: The Problem: This section demonstrates how adding new payment methods leads to code that violates SOLID principles. The Solution: Here, the video introduces the core idea of the Strategy pattern. Applying the Pattern: Watch how the code is refactored into separate strategy classes. The Structure: This part explains the formal UML structure of the pattern.

The structure described in the video can be visualized with the following class diagram for a sorting algorithm scenario.

This diagram shows a `SortingContext` that uses a `SortingStrategy` interface. Concrete strategies like `BubbleSortStrategy` and `QuickSortStrategy` provide different implementations of the `sort` method, allowing the `Client` to switch algorithms dynamically.

Implementation in Spring Boot

Given your background, let's see how this pattern is implemented idiomatically in a modern Spring Boot application. The following article provides a superb, practical example using a payment processing service. It also demonstrates how to leverage Spring's Dependency Injection (DI) to manage and select strategies, which is a powerful technique for building configurable systems.

How to Use Design Patterns in Java with Spring Boot

This article from freeCodeCamp shows a real-world application of the Strategy pattern in Spring Boot, aligning perfectly with your technical stack.

First, read the section What is the Strategy Pattern? to understand the use case. Then, review the code in Step 1, 2, and 3 to see a basic implementation. Next, jump to the section Updated Strategy Pattern. This is the most important part. Pay close attention to how @Component is used to declare strategies as beans and how @Qualifier is used in the PaymentService to inject a specific strategy. This approach is far more flexible than manually instantiating strategies with new.

By using Spring's DI, the PaymentService is completely decoupled from the concrete payment strategies. The decision of which strategy to use can be externalized to configuration files or determined by user roles, making the system highly adaptable.

2. The Observer Pattern: Building Event-Driven Systems

Now, let's shift to a different kind of problem. How do you design a system where a change in one object's state needs to trigger actions in multiple, unrelated objects? For instance, when a new user registers:

  • An EmailService should send a welcome email.
  • An AnalyticsService should log the registration event.
  • A ProfileService should create a default user profile.

A naive implementation would have the UserService directly call methods on all three services. This creates tight coupling; if you later need to add a fourth action (like sending an SMS), you have to modify the UserService again.

The Observer pattern provides a solution by creating a one-to-many dependency between objects. It defines a "Subject" (or Publisher) and a set of "Observers" (or Subscribers). When the Subject's state changes, it automatically notifies all its Observers, which then take action. The Subject doesn't need to know anything about its Observers, only that they implement a common Observer interface.

This UML diagram shows the classic structure. The `Publisher` (Subject) maintains a list of `Subscriber` (Observer) objects. When `mainBusinessLogic` changes the state, it calls `notifySubscribers()`, which in turn calls the `update()` method on each `ConcreteSubscriber`.

A clear analogy is subscribing to a YouTube channel. The channel is the Publisher, and you are a Subscriber. When the channel uploads a new video (a state change), you get a notification (update()). The channel doesn't know who you are personally, it just broadcasts the update to its list of subscribers.

The Observer Pattern in Spring: Application Events

While you can implement the Observer pattern manually, the Spring framework provides a powerful, built-in mechanism that you should use instead: Application Events. This feature is a first-class implementation of the Observer pattern and is the foundation for creating event-driven architectures in Spring.

Let's first see a manual implementation to solidify the concept.

How to Use Design Patterns in Java with Spring Boot

The same freeCodeCamp article also provides a simple, manual implementation of the Observer pattern.

Please read the section What is the Observer Pattern? and review the code. Notice how the UserService (the Subject) holds a list of Observers and manually calls notifyObservers().

Now, let's look at the more powerful, idiomatic Spring approach. The following article is an excellent deep dive into Spring's event system. It will show you how to move from a manual implementation to a professional, feature-rich one using Spring's annotations.

How to Build Event Listeners in Spring - OneUptime

This article from oneuptime.com is a practical guide to leveraging Spring's built-in Observer pattern implementation.

Read through the following sections to understand the core components: How Spring Events Work: This confirms the mechanism follows the Observer pattern. Creating Custom Events: Instead of just a string, you create a dedicated, immutable event object (a POJO) to carry data. This is much more type-safe. Publishing Events: You use the injected ApplicationEventPublisher to fire your event. The UserService is now only coupled to this publisher, not to any listeners. Creating Event Listeners: This is the key. You simply annotate a method in any Spring bean with @EventListener. Spring automatically finds and invokes it when the corresponding event is published. Async Event Listeners: A crucial feature for performance. By adding @Async, you can make a listener run in a separate thread, so the main process (e.g., the user registration request) doesn't have to wait for slow tasks like sending an email. Transactional Event Listeners: This is an advanced and extremely useful feature. The @TransactionalEventListener annotation allows you to tie a listener to a database transaction's lifecycle, for example, ensuring an email is sent only after the user has been successfully saved to the database.

Mastering Spring's event system is a significant step in building robust, decoupled, and event-driven microservices—a hallmark of a senior developer's skill set.

3. Practical Application: Identify Refactoring Opportunities

Your goal is to apply these patterns to improve the architecture of real systems. For this exercise, revisit your current project or the legacy codebase you've been analyzing.

  1. Hunt for a Strategy: Look for if-else or switch statements that choose a behavior or algorithm. Common places include:

    • Different calculation logic based on an entity's type.
    • Varying validation rules for different user levels.
    • Multiple ways to export data (CSV, PDF, JSON).
      In your personal knowledge base, outline a refactoring plan. Define the Strategy interface and the ConcreteStrategy classes you would create. Describe how you would modify the Context class to use Spring's DI with @Qualifier to inject the desired strategy.
  2. Hunt for an Observer: Look for a method where a single action triggers a sequence of calls to different, unrelated services. This indicates tight coupling. A classic example is a create... or update... method that then calls services for notification, logging, indexing, etc.
    In your notes, design a refactoring using Spring Events.

    • Define a custom event class (e.g., OrderPlacedEvent) to carry the relevant data.
    • Show how you would modify the original service to inject and use ApplicationEventPublisher.
    • Define the listener methods in other services using @EventListener. Decide if any of them should be @Async or @TransactionalEventListener and justify your choice.

This exercise will train you to spot these patterns "in the wild" and give you a concrete plan for improving code quality and flexibility.

Conclusion

In this lesson, we explored two powerful behavioral design patterns that are fundamental to building flexible and maintainable systems.

Key Takeaways:

  • Strategy Pattern: Decouples a client from the specific algorithms it uses. It allows you to swap algorithms at runtime, promotes the Open/Closed principle, and eliminates complex conditional logic. In Spring, use DI to manage and inject strategies effectively.
  • Observer Pattern: Creates a decoupled, one-to-many notification system. It is the core of event-driven architecture, allowing publishers to broadcast state changes without knowing their subscribers. In Spring, you should almost always use the built-in Application Events mechanism (ApplicationEventPublisher and @EventListener) to implement this pattern.

You are now equipped with patterns that govern not just the structure of your code, but its runtime behavior and communication flow. These are essential tools for tackling the complex architectural challenges you will face as you transition into a senior role.

In our next lesson, we will address a critical prerequisite for applying these refactoring patterns safely. We'll learn how to write characterization tests, which act as a safety net by capturing the current behavior of legacy code before you begin to change it.

Can't find a good explanation? Sign up and we'll make it for you

Sign up