Strategy

Assume that, depending on conditions, the application must perform different actions. You can extract such a set of interchangeable actions (algorithms) into separate classes. These actions are strategies. The Strategy design pattern lets you change strategies dynamically at runtime.

Thanks to Strategy, you can separate concrete actions from classes. The code becomes clearer and easier to extend. Users don’t need to know all class interfaces – they only set a strategy and call it.

Let’s consider an application that connects to several different APIs. The application must authenticate with each API, but each API requires a different authentication method.

The enum AuthType stores the authentication methods supported by the application:

public enum AuthType {
    BASIC,
    OAUTH,
    JWT;
}

The APIConnector class handles authentication:

public class APIConnector {
    
    public void authBasic (AuthType authType) {
        if (authType.equals(AuthType.BASIC)) {
            System.out.println("Authentication using Basic Auth");
        } else {
            System.err.println("Invalid authentication type");
        }
    }

    public void authOauth (AuthType authType) {
        if (authType.equals(AuthType.OAUTH)) {
            System.out.println("Authentication using OAuth2");
        } else {
            System.err.println("Invalid authentication type");
        }
    }

    public void authJwt (AuthType authType) {
        if (authType.equals(AuthType.JWT)) {
            System.out.println("Authentication using JWT");
        } else {
            System.err.println("Invalid authentication type");
        }
    }
}

Authentication requires creating an APIConnector object and calling the right method:

public class Main {
    public static void main (String[] args) {

        APIConnector connector = new APIConnector();
        connector.authBasic(AuthType.BASIC);
    }
}

This solution has two drawbacks:

  • the user (Main class) must know the interfaces of APIConnector to use it,
  • and each new authentication method requires changes in APIConnector.

You can modify APIConnector to have only one interface:

public class APIConnector {

    public void authenticate (AuthType authType) {
        switch (authType) {
            case BASIC:
                System.out.println("Authentication using Basic Auth");
                break;
            case OAUTH:
                System.out.println("Authentication using OAuth2");
                break;
            case JWT:
                System.out.println("Authentication using JWT");
                break;
            default:
                System.err.println("Unknown authentication type");
        }
    }
} 

Now using APIConnector is easier (Main needs to know only one interface, authenticate()), but each new method still requires a change in APIConnector. The Strategy pattern solves this problem.

Implementation of the Strategy pattern

The AuthStrategy interface declares one method, authenticate():

public interface AuthStrategy {
    void authenticate();
}

The BasicAuthStrategy, OAuthStrategy, and JwtAuthStrategy classes implement the interface. These are concrete strategies. Each one implements authenticate() in a different way and handle different way of authentication:

public class BasicAuthStrategy implements AuthStrategy{
    @Override
    public void authenticate() {
        System.out.println("Authentication using Basic Auth");
    }
}
public class OAuthStrategy implements AuthStrategy{
    @Override
    public void authenticate() {
        System.out.println("Authentication using OAuth2");
    }
}
public class JwtAuthStrategy implements AuthStrategy{
    @Override
    public void authenticate() {
        System.out.println("Authentication using JWT");
    }
}

The APIConnector class has a private field that holds an object that implements AuthStrategy, and a setter. In the authenticate() method you don’t need previous complicated logic. It only calls authenticate() on the object from the field:

public class APIConnector {

    private AuthStrategy authStrategy;

    public void authenticate () {
        this.authStrategy.authenticate();
    }

    public void setAuthStrategy(AuthStrategy authStrategy) {
        this.authStrategy = authStrategy;
    }
}

In the Main class, you still create an APIConnector. Then you set the right strategy and call authenticate() method.

public class Main {
    public static void main (String[] args) {

        APIConnector connector = new APIConnector();

        connector.setAuthStrategy(new BasicAuthStrategy());
        connector.authenticate();
    }
}

Because the field authStrategy is of type AuthStrategy, you can assign any class that implements this interface: BasicAuthStrategy, OAuthStrategy, or JwtAuthStrategy. AuthStrategy is an abstraction layer between APIConnector and the concrete strategies. When you call authenticate(), the app calls the proper implementation automatically.

The AuthType enum is no longer needed.

Adding a new authentication method no longer requires changes in APIConnector. You only add a new strategy.

Summary

The Strategy pattern is a practical application of the open–closed principle and the dependency inversion principle.

Strategy is a popular and simple pattern. It is used when you need flexible changes to behavior. The pattern makes testing, extension, and maintenance easier.


The Strategy pattern and the State pattern both let you change behavior at runtime, but they have different goals:

  • Strategy encapsulates algorithms that are injected from outside and the client chooses which algorithm to use,
  • State defines behavior for each state of an object ahead of time and the change in behavior happens automatically with state changes.