AlgoMaster Logo

Strategy Design Pattern

Ashish

Ashish Pratap Singh

5 min read

The Strategy Design Pattern is a behavioral design pattern that lets you define a family of algorithms, put each one into a separate class, and makes their objects interchangeable — allowing the algorithm to vary independently from the clients that use it.

It’s particularly useful in situations where:

  • You have multiple ways to perform a task or calculation.
  • The behavior of a class needs to change dynamically at runtime.
  • You want to avoid cluttering your code with conditional logic (like if-else or switch statements) for every variation.

When you have multiple ways to achieve the same goal, you might use branching logic inside a class to handle different cases. For example, a PaymentService might use if-else to choose between credit card, cash, or UPI.

But as more payment types are added, this approach becomes hard to scaleviolates the Open/Closed Principle, and makes your code harder to test and maintain.

The Strategy Pattern solves this by encapsulating each behavior in its own class and delegating the responsibility to the right strategy at runtime — keeping your core logic clean, extensible, and testable.

Let’s walk through a real-world example to see how we can apply the Strategy Pattern to build a more flexible and maintainable workflow for handling varying behaviors.

1. The Problem: Shipping Cost Calculation

Imagine you're building a shipping cost calculator for an e-commerce platform.

As with most real-world applications, shipping charges can vary based on different business rules or external providers.

Here are some common strategies you may need to support:

  • Flat Rate: A fixed fee (e.g., $10 per shipment), regardless of weight or destination.
  • Weight-Based: Cost is calculated as a fixed amount per kilogram.
  • Distance-Based: Different rates depending on destination zones (e.g., Zone A = $5, Zone B = $12).
  • Third-Party API: Fetch dynamic rates from providers like FedEx or UPS.

A quick and naive solution might be to implement all of this logic inside a single class, using a long chain of conditionals:

Client Code Using It

What's Wrong with This Approach?

While it may seem fine initially, this design quickly becomes brittle and problematic as your system evolves:

Violates the Open/Closed Principle

Every time a new shipping strategy is added—say “Free Shipping for Prime Members” or “Eco Delivery with Carbon Offset”—you must modify the ShippingCostCalculatorNaive class. This makes the class open to modification rather than being closed for changes and open to extension.

Bloated Conditional Logic

The if-else chain becomes increasingly large and unreadable as more strategies are introduced. It clutters your code and makes debugging harder.

Difficult to Test in Isolation

Each strategy is tangled inside one method, making it harder to test individual behaviors independently. You must set up entire Order objects and manually select the strategy type just to test one case.

Risk of Code Duplication

If different services (e.g., checkout, returns, logistics) use similar logic, you may end up duplicating these conditional blocks in multiple places.

Low Cohesion

The calculator class is doing too much. It knows how to handle every possible algorithm for shipping cost, rather than focusing on orchestrating the calculation.

What we need?

We need a cleaner approach, something that allows:

  • Each shipping strategy to be defined independently
  • Easy plug-and-play behavior at runtime
  • A design that is open for extension, but closed for modification

This is where the Strategy Design Pattern comes into play.

2. The Strategy Design Pattern

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable.

In simpler terms: rather than hardcoding logic with if-else or switch statements, you delegate the behavior to strategy objects.

The pattern allows a client (like a shipping calculator or payment processor) to plug in a specific behavior at runtime without changing the underlying logic of the system.

Class Diagram

Strategy Interface (e.g., ShippingStrategy)

A common interface that declares the algorithm's method. All concrete strategies implement this interface.

Concrete Strategies (e.g., FlatRateShipping, WeightBasedShipping)

These are individual implementations of the Strategy interface . Each class encapsulates a different algorithm.

Context Class (e.g., ShippingCostService)

This is the main class that uses a strategy to perform a task. It holds a reference to a Strategy object and delegates the calculation to it. The context doesn’t know or care which specific strategy is being used—it just knows that it has a strategy that can calculate a shipping cost.

Client

The client is responsible for:

  • Creating the appropriate concrete strategy instance
  • Passing it to the Context
  • Changing the strategy of Context at runtime if needed

3. Implementing Strategy Pattern

Let's refactor our shipping cost calculator.

1. Define the Strategy Interface (ShippingStrategy)

We begin by defining a common interface for all shipping strategies. This allows the context class (ShippingCostService) to interact with any strategy interchangeably.

Each concrete strategy will implement this interface and provide its own algorithm for calculating shipping cost.

2. Implement Concrete Strategy Classes

Each shipping strategy encapsulates its logic in a dedicated class. This makes the behavior modular and easy to extend or test.

FlatRateShipping

WeightBasedShipping

DistanceBasedShipping

ThirdPartyApiShipping

3. Create the Context Class (ShippingCostService)

The context class maintains a reference to a ShippingStrategy and delegates the calculation to it.

This class is completely agnostic to which algorithm it’s using , it simply delegates to whatever strategy is currently set.

4. Client Code

Let’s see how the client dynamically switches between different strategies at runtime:

Notice how clean this is!

  • No conditional logic inside ShippingCostService.
  • Strategies are encapsulated, reusable, and easy to test.
  • Adding a new strategy only requires creating a new class — no changes to the service or existing logic.
  • You can switch strategies at runtime without breaking any existing functionality.

This is the power of the Strategy Pattern: it separates what you do from how you do it, leading to clean, extensible, and maintainable code.