Loading episodes…
0:00 0:00

Visually Explained: Master the Strategy Design Pattern in Java

00:00
BACK TO HOME

Visually Explained: Master the Strategy Design Pattern in Java

10xTeam December 29, 2025 12 min read

Have you ever found yourself trapped in a labyrinth of if-else statements? You start with a simple condition, but as new requirements pour in, the logic branches out, growing into a tangled, unmanageable beast. This is a common scenario, especially when dealing with tasks that have multiple variations or algorithms.

Let’s consider a payroll system. You need to calculate salaries for different types of employees: full-time, hourly, task-based, freelancers, and interns. A naive approach might look like this:

public class SalaryCalculator {
    public BigDecimal calculate(Employee employee) {
        if (employee.getType() == EmployeeType.FULL_TIME) {
            // Full-time logic: base salary + bonus
            return employee.getBaseSalary().add(employee.getBonus());
        } else if (employee.getType() == EmployeeType.HOURLY) {
            // Hourly logic: hours worked * hourly rate
            return employee.getHourlyRate().multiply(BigDecimal.valueOf(employee.getHoursWorked()));
        } else if (employee.getType() == EmployeeType.TASK_BASED) {
            // Task-based logic: tasks completed * rate per task
            return employee.getPayPerTask().multiply(BigDecimal.valueOf(employee.getTasksCompleted()));
        } else if (employee.getType() == EmployeeType.FREELANCER) {
            // Freelancer logic: gross amount - tax - commission
            BigDecimal tax = employee.getGrossAmount().multiply(new BigDecimal("0.10"));
            BigDecimal commission = employee.getGrossAmount().multiply(new BigDecimal("0.05"));
            return employee.getGrossAmount().subtract(tax).subtract(commission);
        } else if (employee.getType() == EmployeeType.INTERN) {
            // Interns get a fixed stipend
            return new BigDecimal("1000.00");
        }
        // And what if a new employee type is added tomorrow? Another 'else if'...
        throw new IllegalArgumentException("Unknown employee type");
    }
}

This code works, but it’s a maintenance nightmare.

  • It violates the Single Responsibility Principle (SRP): The SalaryCalculator class is responsible for every single salary calculation algorithm.
  • It violates the Open/Closed Principle (OCP): To add a new employee type (e.g., “Commissioned”), you must modify this giant if-else block, risking the introduction of bugs into existing, working logic.
  • It’s hard to test: You need to create complex test setups to cover every single branch of this monolithic method.

This is where the Strategy Design Pattern comes to the rescue. It provides an elegant way to clean up this mess.

What is the Strategy Pattern?

The Strategy Pattern is a behavioral design pattern that allows you to define a family of algorithms, encapsulate each one in a separate class, and make their objects interchangeable. Instead of implementing a single algorithm directly, the code receives runtime instructions as to which in a family of algorithms to use.

Think of a golfer. A golfer carries a bag with different clubs (drivers, irons, putters). Depending on the terrain, the distance to the hole, and the desired shot, the golfer selects a strategy by choosing the appropriate club. The action (swinging) is the same, but the chosen club (the strategy) determines the outcome.

Here’s a high-level overview of the pattern’s components:

mindmap
  root((Strategy Pattern))
    Context
      (PayrollService)
      Maintains a reference to a Strategy object.
      Delegates the work to the strategy.
    Strategy Interface
      (ISalaryCalculationStrategy)
      Declares a common interface for all supported algorithms.
    Concrete Strategies
      (FullTimeStrategy)
      (HourlyStrategy)
      (FreelancerStrategy)
      ...
      Each class implements a specific algorithm.

Refactoring the Salary Calculator with the Strategy Pattern

Let’s apply this pattern to our payroll system. We’ll refactor the monolithic SalaryCalculator into a flexible, maintainable, and scalable solution.

Step 1: Define the Project Structure

First, let’s organize our files. We’ll create a dedicated package for our strategies.

src/
└── com/
    └── payroll/
        ├── Employee.java
        ├── EmployeeType.java
        ├── PayrollService.java
        └── strategy/
            ├── ISalaryCalculationStrategy.java
            ├── FullTimeSalaryStrategy.java
            ├── HourlySalaryStrategy.java
            ├── TaskBasedSalaryStrategy.java
            ├── FreelancerSalaryStrategy.java
            └── InternSalaryStrategy.java

Step 2: Create the Strategy Interface

This interface defines the single method that all our calculation algorithms will implement.

[!NOTE] An interface in this context is a contract. It guarantees that every “strategy” class we create will have a calculate method, allowing our PayrollService to use them interchangeably without knowing the specific details of any single one.

// src/com/payroll/strategy/ISalaryCalculationStrategy.java
package com.payroll.strategy;

import com.payroll.Employee;
import java.math.BigDecimal;

/**
 * The Strategy interface declares the method for calculating salary,
 * which is common to all concrete strategy classes.
 */
public interface ISalaryCalculationStrategy {
    BigDecimal calculate(Employee employee);
}

Step 3: Implement Concrete Strategies

Now, we extract each branch of our original if-else statement into its own class. Each class implements the ISalaryCalculationStrategy interface.

Full-Time Strategy:

// src/com/payroll/strategy/FullTimeSalaryStrategy.java
package com.payroll.strategy;

import com.payroll.Employee;
import java.math.BigDecimal;

public class FullTimeSalaryStrategy implements ISalaryCalculationStrategy {
    @Override
    public BigDecimal calculate(Employee employee) {
        System.out.println("Calculating salary for a full-time employee...");
        return employee.getBaseSalary().add(employee.getBonus());
    }
}

Hourly Strategy:

// src/com/payroll/strategy/HourlySalaryStrategy.java
package com.payroll.strategy;

import com.payroll.Employee;
import java.math.BigDecimal;

public class HourlySalaryStrategy implements ISalaryCalculationStrategy {
    @Override
    public BigDecimal calculate(Employee employee) {
        System.out.println("Calculating salary for an hourly employee...");
        return employee.getHourlyRate().multiply(BigDecimal.valueOf(employee.getHoursWorked()));
    }
}

Freelancer Strategy:

// src/com/payroll/strategy/FreelancerSalaryStrategy.java
package com.payroll.strategy;

import com.payroll.Employee;
import java.math.BigDecimal;

public class FreelancerSalaryStrategy implements ISalaryCalculationStrategy {
    private static final BigDecimal TAX_RATE = new BigDecimal("0.10");
    private static final BigDecimal COMMISSION_RATE = new BigDecimal("0.05");

    @Override
    public BigDecimal calculate(Employee employee) {
        System.out.println("Calculating salary for a freelancer...");
        BigDecimal gross = employee.getGrossAmount();
        BigDecimal tax = gross.multiply(TAX_RATE);
        BigDecimal commission = gross.multiply(COMMISSION_RATE);
        return gross.subtract(tax).subtract(commission);
    }
}

…and so on for TaskBasedSalaryStrategy and InternSalaryStrategy. Each class now has a single, clear responsibility.

Step 4: Create the Context

The Context is the class that will use our strategies. In our case, this is the PayrollService. It doesn’t perform the calculation itself; it delegates the task to the strategy object it holds.

// src/com/payroll/PayrollService.java
package com.payroll;

import com.payroll.strategy.ISalaryCalculationStrategy;
import java.math.BigDecimal;

/**
 * The Context class. It is configured with a Concrete Strategy object
 * and maintains a reference to it. It doesn't know the concrete type of a
 * strategy. It should work with all strategies via the Strategy interface.
 */
public class PayrollService {
    private ISalaryCalculationStrategy strategy;

    // The strategy can be set at runtime
    public void setStrategy(ISalaryCalculationStrategy strategy) {
        this.strategy = strategy;
    }

    public BigDecimal calculateSalary(Employee employee) {
        if (strategy == null) {
            throw new IllegalStateException("Strategy not set!");
        }
        return strategy.calculate(employee);
    }
}

Notice how clean this is. The PayrollService has no idea how the salary is calculated. It only knows that it has a strategy object that can do the job.

Step 5: Selecting the Right Strategy

But how does the PayrollService get the correct strategy? We need a mechanism to select the strategy based on the EmployeeType. A Simple Factory is a great pattern for this job.

// src/com/payroll/strategy/SalaryStrategyFactory.java
package com.payroll.strategy;

import com.payroll.EmployeeType;

public class SalaryStrategyFactory {
    public static ISalaryCalculationStrategy getStrategy(EmployeeType type) {
        switch (type) {
            case FULL_TIME:
                return new FullTimeSalaryStrategy();
            case HOURLY:
                return new HourlySalaryStrategy();
            case TASK_BASED:
                return new TaskBasedSalaryStrategy();
            case FREELANCER:
                return new FreelancerSalaryStrategy();
            case INTERN:
                return new InternSalaryStrategy();
            default:
                throw new IllegalArgumentException("Unsupported employee type: " + type);
        }
    }
}

This factory encapsulates the selection logic. If we add a new employee type, we only need to update the EmployeeType enum, create a new strategy class, and add one line to this factory. The PayrollService remains untouched!

Step 6: Putting It All Together (The Client Code)

Now let’s see how the client code changes.

- // Before: The client is coupled to the complex calculation logic
- SalaryCalculator calculator = new SalaryCalculator();
- BigDecimal salary = calculator.calculate(employee);

+ // After: The client delegates the work cleanly
+ // 1. Create the context
+ PayrollService payrollService = new PayrollService();
+
+ // 2. Use the factory to get the correct strategy based on employee type
+ ISalaryCalculationStrategy strategy = SalaryStrategyFactory.getStrategy(employee.getType());
+
+ // 3. Set the strategy in the context
+ payrollService.setStrategy(strategy);
+
+ // 4. Execute the calculation
+ BigDecimal salary = payrollService.calculateSalary(employee);

The client code is now declarative and easy to read. It orchestrates the process without getting bogged down in implementation details.

Visualizing the Final Design

Here are two diagrams that illustrate our new, flexible system.

Class Diagram: This shows the static structure and relationships between our classes.

classDiagram
    class PayrollService {
        -ISalaryCalculationStrategy strategy
        +setStrategy(ISalaryCalculationStrategy)
        +calculateSalary(Employee) BigDecimal
    }
    class SalaryStrategyFactory {
        <<static>>
        +getStrategy(EmployeeType) ISalaryCalculationStrategy
    }
    class Employee {
        -EmployeeType type
    }
    <<interface>> ISalaryCalculationStrategy {
        +calculate(Employee) BigDecimal
    }
    class FullTimeSalaryStrategy {
        +calculate(Employee) BigDecimal
    }
    class HourlySalaryStrategy {
        +calculate(Employee) BigDecimal
    }
    class FreelancerSalaryStrategy {
        +calculate(Employee) BigDecimal
    }

    PayrollService o-- ISalaryCalculationStrategy : uses
    SalaryStrategyFactory ..> ISalaryCalculationStrategy : creates
    FullTimeSalaryStrategy --|> ISalaryCalculationStrategy : implements
    HourlySalaryStrategy --|> ISalaryCalculationStrategy : implements
    FreelancerSalaryStrategy --|> ISalaryCalculationStrategy : implements
    PayrollService ..> Employee
    SalaryStrategyFactory ..> EmployeeType

Sequence Diagram: This shows how the objects interact at runtime to perform a calculation.

sequenceDiagram
    participant Client
    participant PayrollService
    participant SalaryStrategyFactory
    participant ConcreteStrategy

    Client->>SalaryStrategyFactory: getStrategy(employee.getType())
    SalaryStrategyFactory-->>Client: returns strategy instance
    Client->>PayrollService: setStrategy(strategy)
    Client->>PayrollService: calculateSalary(employee)
    PayrollService->>ConcreteStrategy: calculate(employee)
    ConcreteStrategy-->>PayrollService: returns salary
    PayrollService-->>Client: returns salary

When to Use the Strategy Pattern?

Use the Strategy pattern when:

  • You want to use different variants of an algorithm within an object and be able to switch from one algorithm to another during runtime.
  • You have a lot of similar classes that only differ in the way they execute some behavior.
  • You need to isolate the business logic of a class from the implementation details of its algorithms. That way, the logic isn’t cluttered with implementation details.
  • Your class has a massive conditional statement that switches between different variants of the same algorithm.

[!TIP] Strategy Pattern and Dependency Injection

In modern applications using frameworks like Spring, you often don’t need a manual factory. You can define each strategy as a component (e.g., with @Service) and inject a Map<EmployeeType, ISalaryCalculationStrategy>. The framework automatically populates the map, and you can retrieve the correct strategy with map.get(employee.getType()). This is an even more powerful and decoupled approach!

Conclusion

The Strategy Pattern is a powerful tool for transforming complex, conditional logic into clean, maintainable, and extensible code. By encapsulating algorithms into separate, interchangeable classes, you adhere to core SOLID principles and build systems that are prepared for future changes. The next time you find yourself writing a long if-else or switch statement, ask yourself if the Strategy Pattern could be your ticket to a cleaner design.

Quiz: Test Your Knowledge

Question: What is the primary difference between the Strategy pattern and the State pattern?

Answer: Both patterns are structurally similar, using a context and interchangeable objects to change behavior. However, their intent is different.

  • The Strategy pattern is about having different algorithms to accomplish the same goal, and the client is often responsible for choosing and providing the strategy to the context.
  • The State pattern is about altering an object's behavior when its internal state changes. The transitions between states are typically managed by the context or the state objects themselves, not by the client.

Join the 10xdev Community

Subscribe and get 8+ free PDFs that contain detailed roadmaps with recommended learning periods for each programming language or field, along with links to free resources such as books, YouTube tutorials, and courses with certificates.

Audio Interrupted

We lost the audio stream. Retry with shorter sentences?