Strategy Design Pattern in Java

Learn the Strategy pattern with UML diagrams and a Java checkout example that applies different discount policies. Includes runnable code and output.

Strategy Pattern Participants

The Strategy pattern lets us keep different ways of doing a task in separate classes. Each class implements the same interface, so the code that uses these classes can work with any of them. We choose the implementation that provides the behavior we need.

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

For example, a shopping app may offer different discounts to regular customers and members. We can put each discount rule in its own class and give the app’s checkout the rule that applies to the customer. The checkout calculates the total in the same way, while the selected strategy decides the discount.

1. How the Strategy Pattern Works

The Strategy pattern has the following four participants.

Participant What it does
Strategy interface The strategy interface declares the operation that all strategies implement.
Concrete strategies Each concrete strategy implements a different way to perform the shared operation.
Context The context holds a strategy and calls its operation when it needs to perform the task.
Client The client chooses a strategy and gives it to the context.

The client gives the context the strategy it needs. Whenever the context needs to perform the task, it calls a method on that strategy. Because every strategy implements the same interface, the context can use any of them without containing their individual algorithms. The diagram shows how these participants work together.

UML diagram showing Client supplying a strategy to Context, with ConcreteStrategyA and ConcreteStrategyB implementing Strategy

2. Strategy Pattern in Action

Our example is a checkout feature in a shopping app that gives members a ten-percent discount. A regular customer pays the total price of the items in the cart, which we call the subtotal. A member pays that amount after we subtract the discount. We want the checkout code to calculate the final total without checking the customer’s membership itself.

In this example, the discount rules are the strategies, and Checkout is the context. The table maps the other participants to our code.

Pattern participant Class or interface What it does
Strategy interface DiscountPolicy The DiscountPolicy interface declares discountCents(), which returns the discount amount.
Concrete strategy RegularDiscount The RegularDiscount class returns a discount of zero, so the customer pays the full subtotal.
Concrete strategy MemberDiscount The MemberDiscount class returns ten percent of the subtotal as the discount.
Context Checkout The Checkout class holds a policy and uses it in totalCents() to calculate the final total.
Client StrategyDemo The StrategyDemo class creates checkouts with the required policies and runs the examples.

We use the name DiscountPolicy because each implementation represents a rule for calculating discounts. The StrategyDemo class chooses which policy to give the checkout. The diagram below shows the classes and the methods that connect them.

StrategyDemo supplies a DiscountPolicy to Checkout, with RegularDiscount and MemberDiscount implementing discountCents

3. Strategy Pattern Implementation

We first implement the discount rules, then write the checkout code that uses them. We represent all amounts as whole cents, so a value of 10,000 represents 100.00 in the chosen currency. This lets us calculate the discount without using floating-point numbers. We leave out package declarations and imports below; the complete source on GitHub contains the runnable files.

3.1. Define the Strategy Interface

The checkout needs to know how much to subtract from the subtotal. The DiscountPolicy interface provides a discountCents() method for that calculation. The method receives the subtotal in cents and returns how many cents to take off the bill.

public interface DiscountPolicy {
    long discountCents(long subtotalCents);
}

Every implementation of discountCents() returns an amount in cents. For example, if the method returns 1,000, the checkout subtracts 1,000 cents from the subtotal. A policy that returned a percentage instead would give the checkout the wrong information.

3.2. Implement the Discount Rules

The RegularDiscount class returns zero because regular customers do not receive a discount. The MemberDiscount class calculates a ten-percent discount by dividing the subtotal by ten.

public final class RegularDiscount implements DiscountPolicy {
    @Override
    public long discountCents(long subtotalCents) {
        return 0;
    }
}
public final class MemberDiscount implements DiscountPolicy {
    @Override
    public long discountCents(long subtotalCents) {
        return subtotalCents / 10;
    }
}

Both implementations calculate the discount using only the subtotal passed to discountCents(). The MemberDiscount calculation uses integer division, which drops any fraction of a cent from the discount. We will see what that means with a 99-cent subtotal in the demo.

3.3. Use the Policy in the Context

Now we can write the Checkout class. Its constructor receives a discount policy, and its totalCents() method uses that policy to work out what the customer pays. The constructor calls Objects.requireNonNull() to reject a missing policy when we create the checkout.

public final class Checkout {
    private final DiscountPolicy policy;

    public Checkout(DiscountPolicy policy) {
        this.policy = Objects.requireNonNull(policy);
    }

    public long totalCents(long subtotalCents) {
        if (subtotalCents < 0) {
            throw new IllegalArgumentException("Subtotal cannot be negative");
        }

        long discount = policy.discountCents(subtotalCents);
        if (discount < 0 || discount > subtotalCents) {
            throw new IllegalArgumentException("Invalid discount");
        }
        return subtotalCents - discount;
    }
}

The important line is the call to policy.discountCents(subtotalCents). The checkout makes that same call for a regular customer and a member. It also checks that the returned discount is between zero and the subtotal, so an incorrect policy cannot produce a negative bill.

If we later introduce a seasonal discount, we can write another implementation of DiscountPolicy and pass it to a checkout. The checkout stays the same when we add a discount policy that follows this interface. Only the code selecting the policy needs to know about the new option. This illustrates the Open/Closed Principle.

In this example, the calling code chooses a policy and passes it to the Checkout constructor. Each checkout keeps that policy for as long as we use it, so the class does not need a setter to replace the policy.

4. Demo

We create one checkout with RegularDiscount and another with MemberDiscount. We then call totalCents() on both checkouts with a subtotal of 10,000 cents. Finally, we use the member checkout for a purchase of 99 cents to see how the calculation handles a discount that includes a fraction of a cent.

public final class StrategyDemo {
    public static void main(String[] args) {
        Checkout regular = new Checkout(new RegularDiscount());
        Checkout member = new Checkout(new MemberDiscount());

        System.out.println("Regular customer = " + regular.totalCents(10000));
        System.out.println("Member = " + member.totalCents(10000));
        System.out.println("Member with 99 cents = " + member.totalCents(99));
    }
}

After cloning the GitHub repository, we open a terminal in the folder containing src. The following commands compile the example and run StrategyDemo. They also work from the repository root.

javac -d target/strategy-demo -sourcepath src/main/java src/main/java/com/howtodoinjava/patterns/behavioral/strategy/StrategyDemo.java
java -cp target/strategy-demo com.howtodoinjava.patterns.behavioral.strategy.StrategyDemo
Regular customer = 10000
Member = 9000
Member with 99 cents = 90

The regular customer pays the full 10,000 cents, while the member receives a 1,000-cent discount and pays 9,000 cents. Both checkouts use the same total calculation, but the member policy produces a lower price. For the 99-cent purchase, integer division reduces the discount from 9.9 to 9 cents, leaving 90 cents to pay.

5. Similar Patterns

State, Template Method and Command use some of the same techniques as Strategy, but solve different problems. The table shows when each one is useful.

Pattern Main purpose Difference from Strategy
State The State pattern keeps the behavior for each state together, such as what an order can do when it is packed or shipped. With Strategy, the caller chooses a rule; with State, an object’s behavior depends on its current state.
Template Method The Template Method pattern puts a shared sequence of steps in a base class and lets subclasses implement the steps that differ. With Strategy, the context calls a separate strategy object; with Template Method, a base class calls methods that subclasses implement.
Command The Command pattern represents an action, such as removing a song from a playlist, as an object that can be used to perform that action later. A discount strategy provides a calculation rule. A command describes a particular action to carry out.

For our checkout, the choice is between discount rules, so Strategy fits. If we were instead deciding what an order can do before and after shipment, State would be the more relevant pattern.

6. FAQs

These questions come up when choosing where to use Strategy and how much code it needs.

6.1. Can You Give an Example of the Strategy Pattern in Java?

Java’s List.sort() accepts a Comparator that tells it how to compare two elements. We can sort the same list by name or by price by supplying different comparators. The sort operation uses the comparison rule it receives, just as our checkout uses the discount policy it receives.

6.2. How Is Strategy Different From Factory?

A factory creates or selects an object. Strategy separates different ways of performing a task. We can use both together. A factory could select a discount policy for a customer, and the checkout could use that policy to calculate the total.

6.3. Can a Strategy Be a Lambda?

Yes, we can use a lambda to implement a strategy when its interface has one abstract method. Our DiscountPolicy meets that condition, so we can pass subtotal -> subtotal / 10 to the checkout instead of creating a MemberDiscount object. A short calculation can be clear as a lambda. A named class is often easier to follow when the calculation becomes more complex or needs another service. The Java lambda guide explains how lambdas implement functional interfaces.

7. Conclusion

In this checkout, calculating the total and deciding the discount are separate jobs. The checkout calculates the total, while the supplied policy decides how much to take off. When we need another discount, we can add a policy and pass it to the checkout. If there is only one simple rule, we can keep it in a regular method until separating it serves a useful purpose.

Happy Learning!

Leave a Comment

  1. Hi Sir,

    I have been following your blogs for a long time and these are really very helpful.
    I have just one doubt.
    what is the need of creating context class? because anyway we can simply create the implementation class object(either directly or by using factory pattern) and pass the name of the friend we want to connect with.
    for example :-

    // Setting Facebook strategy.

    SocialmediaStrategy facebookStrategy= SocialmediaStrategyFactory.getBean(“facebook”);

    facebookStrategy.connectTo(“Lokesh”);

    ============================
    SocialmediaStrategy twitterStrategy= SocialmediaStrategyFactory.getBean(“twitter”);

    twitterStrategy.connectTo(“Lokesh”);

    Thanks.

    • Good question. Yes, we can write the code using the factory pattern as well because a factory is also used to create classes having similar features.

      The context class just neatly provides the abstraction that you will appreciate once you use dependency injection for setting various strategies.

  2. Thanks for the explanation. I built a similar solution for 2 implementation strategies of connecting to HTTP or S FTP . Do you think, this is a slightly better way of implementing Strategy Design pattern? The code that I wrote would not require the user to know about the name of the Implementation class. In the future if another implementation class gets added, the user would need not know the name of the class.
    Here goes my code:

    1. Strategy Interface

    public interface IConnectionStrategy {
    
    	/**Get Implementation type*/
    	public boolean getTypeOfConnectionStrategy(String type);	
    
    	/**	concrete method to test connection */
    	public boolean testConnection();
    }
    
    

    2. Implementation 1 : HTTP

    public class HTTPConnectionStrategy implements IConnectionStrategy {
    	@Override
    	public boolean testConnection() {
    		// establish connection code goes here
    		return true;
    	}
    	@Override
    	public boolean getTypeOfConnectionStrategy(String type) {
    		return (type.equals("HTTP"));
    	}
    }
    

    2. Implementation 2: SFTP

    public class SFTPConnectionStrategy implements IConnectionStrategy {
    	@Override
    	public boolean testConnection() {
    		// establish connection code goes here
    		return true;
    	}
    	@Override
    	public boolean getTypeOfConnectionStrategy(String type) {
    		return (type.equals("SFTP"));
    	}
    }
    

    3. Strategy Context class

    /**
     *Strategy Context to determine strategy at run time */
    public class StrategyContext {
    	//externally initialized
    	List<IConnectionStrategy> connexonStrategies;
    	/**Test the connection based on type*/
    	public boolean testConnection(String argType) {
    		boolean isTestSuccessful=false;
    		for (IConnectionStrategy iConnectionStrategy : connexonStrategies) {
    			if(iConnectionStrategy.getTypeOfConnectionStrategy(argType)) {
    				isTestSuccessful =  iConnectionStrategy.testConnection();
    			}
    		}
    		return isTestSuccessful;
    	}
    }
    
    class TestClass{
    	// Testing Connection
    	public static void main(String[] args) {
    		StrategyContext strategyContext = new StrategyContext();
    		strategyContext.testConnection("SFTP");
    	}
    }
    
    • it is not a better implementing because it break the “open-close” principle.once you add a new strategy you have to modify the existing code, for example, you have to add the “argType”.

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.