AlgoMaster Logo

Strategy Design Pattern

Last Updated: December 31, 2025

Ashish

Ashish Pratap Singh

5 min read

At its core, the Strategy pattern is about separating "what varies" from "what stays the same."

Instead of embedding multiple algorithms inside a single class with conditional logic, you extract each algorithm into its own strategy class. The main class (context) delegates the work to whichever strategy is currently plugged in.

This pattern becomes valuable when:

  • You have multiple ways to perform the same operation, and the choice might change at runtime
  • You want to avoid bloated conditional statements that select between different behaviors
  • You need to isolate algorithm-specific data and logic from the code that uses it
  • Different clients might need different algorithms for the same task

A common first approach is to use if-else or switch statements inside a class to handle different behaviors. If you are building a payment system, you might check "is it credit card? PayPal? cryptocurrency?" and handle each case inline.

This works initially, but as the number of cases grows, your class becomes a maintenance nightmare.

The Strategy pattern solves this by turning each branch of that conditional into a separate class. The main class holds a reference to a strategy interface and calls it without knowing which concrete implementation is being used.

You can swap strategies at runtime, add new ones without touching existing code, and test each algorithm in isolation.

Let us walk through a real-world example to see how the Strategy Pattern transforms messy conditional code into a clean, extensible design.

1. The Problem: Shipping Cost Calculation

Imagine you are building an e-commerce platform. One of the features you need is a shipping cost calculator. Sounds simple enough, but shipping costs can be calculated in many different ways depending on business rules:

  • Flat Rate: A fixed fee regardless of weight or distance
  • Weight-Based: Cost increases with package weight
  • Distance-Based: Different rates for different delivery zones
  • Express Delivery: Premium pricing for faster service
  • Third-Party API: Dynamic rates from carriers like FedEx or UPS

Your first implementation might look like this:

Client Code Using It

This works. The client passes a method name, and the calculator returns the appropriate cost. But watch what happens as the business evolves.

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 new shipping method requires modifying the ShippingCalculator class. You are constantly opening a class that should be stable. Each modification risks breaking existing functionality.

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

What if another part of your application needs shipping calculations? You might copy this logic, and now you have two places to maintain.

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 Really Need

We need an approach where:

  • Each shipping algorithm lives in its own class
  • Adding a new algorithm does not require modifying existing classes
  • The calculator does not need to know which algorithm it is using
  • Algorithms can be swapped at runtime based on user preferences or business rules
  • Each algorithm can be tested independently

This is exactly what the Strategy Pattern provides.

2. Understanding the Strategy Pattern

The Strategy Pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it.

In plain terms: instead of a giant if-else chain deciding which algorithm to run, you create separate classes for each algorithm. These classes all implement the same interface. The client holds a reference to this interface and calls it without knowing which concrete class is behind it.

The Structure

The Strategy Pattern involves three key components:

Strategy Interface (e.g., ShippingStrategy)

Declares the interface common to all supported algorithms. The Context uses this interface to call the algorithm defined by a ConcreteStrategy.

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

Implements the algorithm using the Strategy interface. Each concrete strategy encapsulates a specific 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.

How It Works

  1. The client creates a concrete strategy object
  2. The client passes the strategy to the context
  3. The context stores a reference to the strategy
  4. When the context needs to run the algorithm, it delegates to the strategy
  5. The client can swap the strategy at any time

3. Implementing the Strategy Pattern

Let us refactor our shipping calculator using the Strategy Pattern.

Step 1: Define the Strategy Interface (ShippingStrategy)

First, we define a common interface that all shipping strategies must implement:

This interface is simple and focused. Every strategy takes an order and returns a cost. The interface says nothing about how the cost is calculated.

Step 2: Implement Concrete Strategies

Each shipping algorithm becomes its own class:

FlatRateShipping

WeightBasedShipping

DistanceBasedShipping

ThirdPartyApiShipping

Notice how each class is focused on a single responsibility. The DistanceBasedShipping class knows about zones. The WeightBasedShipping class knows about weight calculations. Neither knows about the other.

Step 3: Create the Context Class

The context class holds a reference to a strategy and delegates calculations to it:

The context is simple. It stores a strategy, provides a way to change it, and delegates calculations. It does not know or care which concrete strategy is being used.

Step 4: Client Code

Here is how the client uses the pattern:

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.

4. What We Gained

Let us evaluate what the Strategy Pattern has given us:

Open/Closed Principle

The ShippingCostCalculator is now closed for modification. To add a new shipping method, you create a new strategy class. The existing code remains untouched.

Single Responsibility

Each strategy class has one job: calculate shipping cost using a specific algorithm. The calculator has one job: orchestrate the calculation by delegating to a strategy.

Testability

Each strategy can be unit tested in isolation. You do not need to set up complex scenarios to reach a specific branch. Just create the strategy and call calculateCost().

Runtime flexibility

Strategies can be swapped at any time. A user might start with standard shipping and upgrade to express during checkout. The system handles this seamlessly.

No string-based dispatch

We use type-safe strategy objects instead of fragile string comparisons. The compiler catches mistakes.

Composition over inheritance

The calculator and strategies are separate objects. Changes to one do not ripple through the others.