Skip to content
Dev Dump

Behavioral Patterns

Behavioral patterns focus on the assignment of responsibilities between objects and how they communicate. They manage complex control flows and algorithm structures, shifting the developer’s focus from the specific objects to how they interact.


Intent: Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.

When you have a class performing a specific task in different ways (like sorting, or paying), do not use giant if-else or switch statements. Instead, extract the algorithms into separate classes (Strategies) that implement a common interface.

Strategy Pattern The Context maintains a reference to a Strategy interface. It can dynamically swap out the concrete strategy at runtime.

Strategy Sequence Diagram Sequence diagram: Client swaps strategies at runtime, Context delegates to current strategy.

Without Pattern - Conditionals everywhere, hard to extend:

class ShoppingCart {
public void checkout(int amount, String paymentType) {
// Problem: Adding new payment = modify this method!
if (paymentType.equals("creditcard")) {
System.out.println("Connecting to credit card gateway...");
System.out.println("Paid " + amount + " via Credit Card.");
} else if (paymentType.equals("paypal")) {
System.out.println("Redirecting to PayPal...");
System.out.println("Paid " + amount + " via PayPal.");
} else if (paymentType.equals("crypto")) {
// Adding this required changing ShoppingCart!
System.out.println("Paid " + amount + " via Crypto.");
}
// More payment types = bigger if-else mess!
}
}

With Pattern - Open for extension, closed for modification:

// 1. The Strategy Interface
interface PaymentStrategy {
void pay(int amount);
}
// 2. Concrete Strategies - each in its own class
class CreditCardPayment implements PaymentStrategy {
public void pay(int amount) {
System.out.println("Paid " + amount + " via Credit Card.");
}
}
class PaypalPayment implements PaymentStrategy {
public void pay(int amount) {
System.out.println("Paid " + amount + " via PayPal.");
}
}
// Adding new payment = just add new class, NO changes to ShoppingCart!
class CryptoPayment implements PaymentStrategy {
public void pay(int amount) {
System.out.println("Paid " + amount + " via Crypto.");
}
}
// 3. The Context - never needs to change
class ShoppingCart {
private PaymentStrategy strategy;
public void setPaymentStrategy(PaymentStrategy strategy) {
this.strategy = strategy;
}
public void checkout(int amount) {
strategy.pay(amount); // Delegates to whatever strategy is set
}
}
// Usage - swap strategies at runtime!
ShoppingCart cart = new ShoppingCart();
cart.setPaymentStrategy(new CreditCardPayment());
cart.checkout(100); // Paid 100 via Credit Card.
cart.setPaymentStrategy(new CryptoPayment());
cart.checkout(50); // Paid 50 via Crypto.
  • Eliminates conditionals: No more if (type == "creditcard") scattered everywhere
  • Open/Closed: Add new strategies without modifying existing code
  • Runtime flexibility: Swap algorithms on the fly based on user choice or config

Intent: Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.

Often used in UI frameworks, event-handling systems, and Pub/Sub architectures. Instead of objects polling a data source repeatedly (“Are you done yet?”), the data source pushes updates to the objects when ready.

Observer Pattern The Subject maintains a list of Observers. When its state changes, it loops through and calls update() on all of them.

Observer Pattern Metaphor Like a newsletter: subscribers register, and all get notified when news is published.

Observer Sequence Diagram Sequence diagram: Observers subscribe, Subject broadcasts update() to all.

Without Pattern - Tight coupling, polling required:

class NewsAgency {
private String news;
public String getNews() { return news; }
public void setNews(String news) { this.news = news; }
}
class EmailClient {
private NewsAgency agency;
private String lastNews = "";
// Problem: Must constantly poll for updates!
public void checkForUpdates() {
String current = agency.getNews();
if (!current.equals(lastNews)) {
System.out.println("Email: " + current);
lastNews = current;
}
}
}
// Usage - wasteful polling
while (true) {
emailClient.checkForUpdates(); // Check every second
smsClient.checkForUpdates(); // Each client polls separately
appClient.checkForUpdates(); // Wastes resources!
Thread.sleep(1000);
}

With Pattern - Push notifications, loose coupling:

// 1. Observer Interface
interface Observer {
void update(String news);
}
// 2. Concrete Observers
class EmailSubscriber implements Observer {
public void update(String news) {
System.out.println("Email received: " + news);
}
}
class SMSSubscriber implements Observer {
public void update(String news) {
System.out.println("SMS received: " + news);
}
}
// 3. Subject Interface
interface Subject {
void attach(Observer o);
void detach(Observer o);
void notifyObservers();
}
// 4. Concrete Subject
class NewsAgency implements Subject {
private List<Observer> subscribers = new ArrayList<>();
private String latestNews;
public void attach(Observer o) { subscribers.add(o); }
public void detach(Observer o) { subscribers.remove(o); }
public void setNews(String news) {
this.latestNews = news;
notifyObservers(); // Automatically notify all!
}
public void notifyObservers() {
for (Observer o : subscribers) {
o.update(latestNews);
}
}
}
// Usage - no polling needed!
NewsAgency agency = new NewsAgency();
agency.attach(new EmailSubscriber());
agency.attach(new SMSSubscriber());
agency.setNews("Breaking: Design Patterns are awesome!");
// Output:
// Email received: Breaking: Design Patterns are awesome!
// SMS received: Breaking: Design Patterns are awesome!
  • Loose coupling: Subject and observers are independent—add/remove observers freely
  • Broadcast communication: One event triggers multiple reactions automatically
  • Push model: No polling; updates are pushed when state changes

Intent: Allow an object to alter its behavior when its internal state changes. The object will appear to change its class.

Similar to the Strategy pattern, but here, the specific strategies (States) are aware of each other and can trigger state transitions within the Context. It elegantly replaces massive state-machine if-else blocks.

State Pattern The Context delegates state-specific behavior to the current State object. State objects can replace the Context’s active state.

State Sequence Diagram Sequence diagram: State objects trigger transitions, changing Context behavior.

A traffic light demonstrates cyclic state transitions: Red → Green → Yellow → Red → …

// 1. State Interface
interface TrafficLightState {
void change(TrafficLight light);
}
// 2. Concrete States
class RedState implements TrafficLightState {
@Override
public void change(TrafficLight light) {
System.out.println("Red → Green");
light.setState(new GreenState());
}
}
class GreenState implements TrafficLightState {
@Override
public void change(TrafficLight light) {
System.out.println("Green → Yellow");
light.setState(new YellowState());
}
}
class YellowState implements TrafficLightState {
@Override
public void change(TrafficLight light) {
System.out.println("Yellow → Red");
light.setState(new RedState());
}
}
// 3. Context Class
class TrafficLight {
private TrafficLightState state;
public TrafficLight() {
state = new RedState(); // Initial state
}
public void setState(TrafficLightState state) {
this.state = state;
}
public void change() {
state.change(this);
}
}
// 4. Usage
public class Main {
public static void main(String[] args) {
TrafficLight light = new TrafficLight();
light.change(); // Red → Green
light.change(); // Green → Yellow
light.change(); // Yellow → Red
light.change(); // Red → Green
}
}

Output:

Red → Green
Green → Yellow
Yellow → Red
Red → Green
  • Eliminates state conditionals: Each state class handles its own behavior
  • Explicit transitions: State changes are clear and controlled
  • Single Responsibility: Each state class focuses on one state’s behavior

Intent: Encapsulate a request as an object, thereby allowing developers to parameterize clients with queues, requests, and implement undoable operations.

Instead of a button directly calling a method on a device, the button triggers a Command object. This creates a powerful layer of separation, unlocking features like command queuing, logging, and Undo/Redo mechanisms.

Command Pattern The Invoker executes the Command. The Command knows exactly which method to call on the Receiver.

Command Pattern Metaphor Like restaurant orders: tickets (commands) queue up, enabling undo and history.

Command Sequence Diagram Sequence diagram: Command encapsulates request, enabling execute() and undo().

Without Pattern - Tight coupling, no undo:

class RemoteControl {
private Light light;
private Fan fan;
private TV tv;
// Problem: Remote knows about every device!
public void pressLightButton() {
light.turnOn(); // Direct coupling
}
public void pressFanButton() {
fan.start(); // Direct coupling
}
// Can't undo! Can't queue! Can't log!
// Adding new device = modify RemoteControl!
}

With Pattern - Decoupled, undoable, queueable:

// 1. Command Interface
interface Command {
void execute();
void undo();
}
// 2. The Receiver (The actual device)
class Light {
public void turnOn() { System.out.println("Light is ON"); }
public void turnOff() { System.out.println("Light is OFF"); }
}
class Fan {
public void start() { System.out.println("Fan started"); }
public void stop() { System.out.println("Fan stopped"); }
}
// 3. Concrete Commands - one per action
class TurnOnLightCommand implements Command {
private Light light;
public TurnOnLightCommand(Light light) {
this.light = light;
}
public void execute() { light.turnOn(); }
public void undo() { light.turnOff(); }
}
class StartFanCommand implements Command {
private Fan fan;
public StartFanCommand(Fan fan) {
this.fan = fan;
}
public void execute() { fan.start(); }
public void undo() { fan.stop(); }
}
// 4. Invoker - doesn't know what it controls!
class RemoteControl {
private Stack<Command> history = new Stack<>();
public void pressButton(Command cmd) {
cmd.execute();
history.push(cmd); // Save for undo
}
public void pressUndo() {
if (!history.isEmpty()) {
history.pop().undo();
}
}
}
// Usage
RemoteControl remote = new RemoteControl();
Light light = new Light();
Fan fan = new Fan();
remote.pressButton(new TurnOnLightCommand(light)); // Light is ON
remote.pressButton(new StartFanCommand(fan)); // Fan started
remote.pressUndo(); // Fan stopped
remote.pressUndo(); // Light is OFF
  • Decouples invoker from receiver—button doesn’t know what it controls
  • Enables Undo/Redo: Commands can store state for reversal
  • Queueable: Commands are objects that can be stored, logged, or scheduled