Strategy Pattern
The Strategy Pattern is a behavioral design pattern that allows developers to define a family of algorithms or behaviors, place each one in a separate class, and make them interchangeable at runtime. Instead of writing large conditional blocks to choose between different behaviors, the Strategy Pattern lets the application select the correct strategy object and use it through a common interface.
This pattern is very useful in Object-Oriented Programming when an application has multiple ways to perform the same task. Examples include payment methods, discount calculations, shipping cost rules, sorting algorithms, export formats, notification channels, validation rules, and pricing strategies.
Introduction
In previous articles of this Design Patterns series, we discussed creational patterns such as Factory, Abstract Factory, Builder, and Prototype, as well as structural patterns such as Adapter, Decorator, Facade, and Repository. These patterns help developers create objects, connect systems, and organize code.
The Strategy Pattern belongs to the behavioral design patterns category. Behavioral patterns focus on how objects communicate, how responsibilities are distributed, and how behavior can change without making the code difficult to maintain.
Strategy is one of the most practical behavioral patterns because many real applications need to switch between different rules or algorithms. Without Strategy Pattern, this logic often becomes a long list of if statements or switch cases. With Strategy Pattern, each behavior is placed in its own class, making the code cleaner and easier to extend.
What Is the Strategy Pattern?
The Strategy Pattern is a design pattern that defines a common interface for a group of interchangeable behaviors. Each behavior is implemented in a separate class called a strategy. The main class, often called the context, uses the strategy through the interface without knowing the exact implementation details.
In simple terms, Strategy Pattern allows the application to choose how something should be done without changing the class that uses the behavior.
For example, an e-commerce application may calculate discounts differently for regular customers, premium customers, seasonal campaigns, and coupon codes. Instead of placing all discount rules inside one large method, each discount rule can be implemented as a separate strategy.
Why the Strategy Pattern Is Important
The Strategy Pattern is important because it helps reduce complex conditional logic. When many behaviors are handled inside one class using if or switch statements, the class becomes harder to read, test, and extend.
Every time a new behavior is added, the original class must be modified. This increases the risk of breaking existing behavior. Strategy Pattern solves this by allowing new behavior to be added as a new class that follows the same interface.
This supports the Open Closed Principle: software should be open for extension but closed for modification.
Problem Without Strategy Pattern
Imagine an application that calculates shipping cost based on delivery method. Without Strategy Pattern, the code may look like this:
class ShippingCalculator
{
public function calculate(string $method, float $weight): float
{
if ($method === 'standard') {
return $weight * 2;
}
if ($method === 'express') {
return $weight * 5;
}
if ($method === 'international') {
return $weight * 10 + 20;
}
throw new InvalidArgumentException('Invalid shipping method.');
}
}This code works, but it becomes harder to maintain when more shipping methods are added. The ShippingCalculator class must change every time a new method is introduced.
Strategy Pattern moves each shipping calculation into its own class.
Basic Strategy Pattern Example in PHP
First, define a common strategy interface:
interface ShippingStrategy
{
public function calculate(float $weight): float;
}Now create concrete strategy classes:
class StandardShippingStrategy implements ShippingStrategy
{
public function calculate(float $weight): float
{
return $weight * 2;
}
}
class ExpressShippingStrategy implements ShippingStrategy
{
public function calculate(float $weight): float
{
return $weight * 5;
}
}
class InternationalShippingStrategy implements ShippingStrategy
{
public function calculate(float $weight): float
{
return ($weight * 10) + 20;
}
}Each strategy contains one specific shipping calculation rule. The rules are separated and easier to test.
Creating the Context Class
The context class uses the strategy through the interface:
class ShippingCalculator
{
public function __construct(
private ShippingStrategy $strategy
) {
}
public function calculate(float $weight): float
{
return $this->strategy->calculate($weight);
}
}The ShippingCalculator does not know whether it is using standard, express, or international shipping. It only knows that it has a ShippingStrategy.
Using the Strategy Pattern
The application can choose the correct strategy and pass it to the context:
$strategy = new ExpressShippingStrategy();
$calculator = new ShippingCalculator($strategy);
$cost = $calculator->calculate(3.5);The same ShippingCalculator can work with any strategy that implements the ShippingStrategy interface.
If a new delivery method is added later, a new strategy class can be created without modifying the ShippingCalculator.
Main Parts of the Strategy Pattern
The Strategy Pattern usually includes these main parts:
Strategy interface: Defines the common method that all strategies must implement.
Concrete strategies: Classes that implement different versions of the behavior or algorithm.
Context: The class that uses a strategy object to perform the behavior.
Client code: The code that selects or provides the correct strategy.
This structure separates behavior selection from behavior execution.
Real-World Example: Discount Strategy
Discount calculation is one of the most common examples of Strategy Pattern. Different customers or campaigns may use different discount rules.
interface DiscountStrategy
{
public function calculate(float $total): float;
}
class NoDiscountStrategy implements DiscountStrategy
{
public function calculate(float $total): float
{
return 0;
}
}
class RegularCustomerDiscountStrategy implements DiscountStrategy
{
public function calculate(float $total): float
{
return $total * 0.05;
}
}
class PremiumCustomerDiscountStrategy implements DiscountStrategy
{
public function calculate(float $total): float
{
return $total * 0.15;
}
}
class SeasonalDiscountStrategy implements DiscountStrategy
{
public function calculate(float $total): float
{
return $total * 0.25;
}
}Each discount rule is now placed in a separate class. This makes it easier to add, remove, or modify discount behavior.
Using Discount Strategy
class DiscountService
{
public function __construct(
private DiscountStrategy $strategy
) {
}
public function getFinalTotal(float $total): float
{
$discount = $this->strategy->calculate($total);
return $total - $discount;
}
}
$service = new DiscountService(new PremiumCustomerDiscountStrategy());
$finalTotal = $service->getFinalTotal(200);The DiscountService does not need to contain all possible discount rules. It only uses the selected strategy.
Real-World Example: Payment Strategy
Payment systems are another practical use case. An application may support different payment methods such as credit card, PayPal, bank transfer, or cash on delivery.
interface PaymentStrategy
{
public function pay(float $amount): bool;
}
class CreditCardPaymentStrategy implements PaymentStrategy
{
public function pay(float $amount): bool
{
// Pay by credit card
return true;
}
}
class PayPalPaymentStrategy implements PaymentStrategy
{
public function pay(float $amount): bool
{
// Pay by PayPal
return true;
}
}
class BankTransferPaymentStrategy implements PaymentStrategy
{
public function pay(float $amount): bool
{
// Pay by bank transfer
return true;
}
}Each payment method has its own strategy. The checkout process can use any payment strategy through the same interface.
Checkout with Payment Strategy
class CheckoutService
{
public function __construct(
private PaymentStrategy $paymentStrategy
) {
}
public function checkout(float $amount): bool
{
return $this->paymentStrategy->pay($amount);
}
}
$checkout = new CheckoutService(new PayPalPaymentStrategy());
$checkout->checkout(150);This design makes it easy to add new payment methods later without rewriting the CheckoutService.
Choosing a Strategy at Runtime
In real applications, the selected strategy may depend on user input, configuration, database values, request parameters, or business rules.
A simple factory can be used to choose the correct strategy:
class PaymentStrategyFactory
{
public function create(string $method): PaymentStrategy
{
return match ($method) {
'credit_card' => new CreditCardPaymentStrategy(),
'paypal' => new PayPalPaymentStrategy(),
'bank_transfer' => new BankTransferPaymentStrategy(),
default => throw new InvalidArgumentException('Invalid payment method.'),
};
}
}The Factory Pattern can create the correct Strategy object, and the Strategy Pattern handles the behavior. These two patterns often work well together.
Strategy Pattern with Dependency Injection
The Strategy Pattern works well with dependency injection. Instead of creating a strategy inside the context class, the strategy is passed from outside.
This makes the context class easier to test and easier to configure. During tests, developers can pass a fake strategy. In production, the application can pass the real strategy selected by configuration or user choice.
class ExportService
{
public function __construct(
private ExportStrategy $strategy
) {
}
public function export(array $data): string
{
return $this->strategy->export($data);
}
}The ExportService depends on an abstraction and remains independent from concrete export formats.
Real-World Example: Export Strategy
An application may export data in different formats such as PDF, Excel, CSV, or JSON. Each format requires different logic.
interface ExportStrategy
{
public function export(array $data): string;
}
class PdfExportStrategy implements ExportStrategy
{
public function export(array $data): string
{
return 'PDF export content';
}
}
class CsvExportStrategy implements ExportStrategy
{
public function export(array $data): string
{
return 'CSV export content';
}
}
class JsonExportStrategy implements ExportStrategy
{
public function export(array $data): string
{
return json_encode($data);
}
}Instead of placing all export logic in one class, each format has its own strategy. This improves maintainability and makes each export format easier to test.
Strategy Pattern in Laravel
In Laravel applications, Strategy Pattern is useful for payment gateways, discount rules, shipping methods, notification channels, file exporters, validation rules, and report generators.
Laravel's service container can inject a strategy implementation based on configuration. For example, a service provider can bind an interface to a specific strategy:
$this->app->bind(
PaymentStrategy::class,
PayPalPaymentStrategy::class
);For dynamic selection based on request data, a factory or resolver class can choose the correct strategy and pass it to the service.
Laravel Strategy Resolver Example
class PaymentStrategyResolver
{
public function resolve(string $method): PaymentStrategy
{
return match ($method) {
'credit_card' => app(CreditCardPaymentStrategy::class),
'paypal' => app(PayPalPaymentStrategy::class),
'bank_transfer' => app(BankTransferPaymentStrategy::class),
default => throw new InvalidArgumentException('Invalid payment method.'),
};
}
}This keeps the controller clean. The controller can ask the resolver for the correct strategy instead of containing payment selection logic.
Strategy Pattern in Symfony
Symfony applications can use Strategy Pattern through services and dependency injection. Different strategy classes can be registered as services, and a resolver can select the correct one based on configuration or runtime input.
Symfony tags can also be used to collect multiple strategy services and organize them by key. This is useful in large applications where strategies should be extendable without modifying one large factory class.
Strategy Pattern fits naturally with Symfony service architecture and helps keep business logic clean.
Strategy Pattern and Open Closed Principle
The Strategy Pattern strongly supports the Open Closed Principle. The context class is closed for modification because it does not need to change when new strategies are added. The system remains open for extension because developers can add new strategy classes.
For example, if an application supports PDF and CSV export, adding Excel export can be done by creating an ExcelExportStrategy class that implements the same interface. The ExportService does not need to change.
This makes the design safer and easier to maintain over time.
Strategy Pattern and Polymorphism
Strategy Pattern depends heavily on polymorphism. All strategy classes implement the same interface, but each class provides different behavior.
The context class uses the interface, and the actual behavior depends on the concrete strategy object passed at runtime.
This is a practical example of polymorphism in real software design. The same method call can produce different results depending on the selected strategy.
Strategy Pattern vs Factory Pattern
Strategy Pattern and Factory Pattern are different, but they are often used together.
The Factory Pattern focuses on object creation. It decides which object should be created. The Strategy Pattern focuses on behavior. It defines interchangeable algorithms or rules.
For example, a PaymentStrategyFactory may create a PayPalPaymentStrategy. The factory handles creation, while the strategy handles payment behavior.
In short, Factory creates the strategy, and Strategy performs the behavior.
Strategy Pattern vs State Pattern
Strategy Pattern and State Pattern can look similar because both use interchangeable objects. However, their intent is different.
Strategy Pattern allows the client or application to choose an algorithm or behavior. The selected strategy usually represents a choice such as payment method, discount type, or export format.
State Pattern changes behavior based on the internal state of an object. For example, an order may behave differently when it is pending, paid, shipped, or cancelled.
In short, Strategy is about selecting behavior, while State is about behavior changing as an object moves through states.
Strategy Pattern vs Template Method Pattern
The Template Method Pattern uses inheritance to define the structure of an algorithm in a parent class while allowing child classes to customize specific steps.
The Strategy Pattern uses composition and interfaces to make entire behaviors interchangeable.
Strategy is usually more flexible because strategies can be changed at runtime. Template Method is useful when the algorithm structure is fixed and only certain steps vary.
Benefits of Strategy Pattern
The Strategy Pattern provides many benefits in object-oriented software design.
Main benefits include:
Reduces large if and switch statements.
Separates different algorithms into focused classes.
Makes behavior easier to test independently.
Supports the Open Closed Principle.
Improves flexibility by allowing runtime behavior selection.
Works well with dependency injection and interfaces.
Improves readability and maintainability.
Makes adding new behavior safer and cleaner.
These benefits make Strategy Pattern one of the most useful behavioral design patterns.
Drawbacks of Strategy Pattern
The Strategy Pattern can also have drawbacks. It may increase the number of classes in the project because each behavior usually needs its own class.
For simple logic with only one or two small conditions, Strategy Pattern may be unnecessary. Adding interfaces and many classes for a very small problem can make the code harder to understand.
Another drawback is that the client code or a resolver must choose the correct strategy. If strategy selection logic becomes complex, it should be organized carefully in a factory or resolver.
When to Use Strategy Pattern
Use Strategy Pattern when an application has multiple ways to perform the same task and those behaviors may change or grow over time.
Strategy Pattern is useful when:
You have multiple algorithms for the same operation.
You want to avoid large conditional blocks.
Behavior should be selected at runtime.
New behaviors may be added in the future.
You want to test each behavior separately.
The main class should not know all implementation details.
You want to follow interface-based design.
If these conditions exist, Strategy Pattern can improve the design significantly.
When Not to Use Strategy Pattern
Do not use Strategy Pattern when the behavior is simple and unlikely to change. If there is only one algorithm or the conditions are very small, direct code may be clearer.
Avoid Strategy Pattern when:
There is only one behavior implementation.
The logic is too simple to justify extra classes.
The pattern makes the project harder to navigate.
The behavior does not need to change dynamically.
A simple method or configuration value is enough.
Design patterns should make the code cleaner, not more complicated.
Common Mistakes with Strategy Pattern
One common mistake is creating strategies for very small differences that do not need separate classes. This can create unnecessary complexity.
Another mistake is placing strategy selection logic inside the context class. The context should use the strategy, not contain a large switch statement to choose it.
A third mistake is making strategy interfaces too broad. A strategy interface should define only the behavior that all strategies truly share.
A fourth mistake is mixing business workflows with strategy logic. Each strategy should focus on one interchangeable behavior.
Best Practices for Strategy Pattern
To use Strategy Pattern effectively, developers should keep strategies focused, clear, and easy to replace.
Useful best practices include:
Define a small and clear strategy interface.
Keep each strategy focused on one behavior.
Use dependency injection to provide strategies.
Use a factory or resolver to choose strategies when needed.
Avoid putting strategy selection logic inside the context class.
Use meaningful names such as PayPalPaymentStrategy or PremiumDiscountStrategy.
Test each strategy independently.
Do not overuse Strategy Pattern for simple conditions.
These practices help keep Strategy Pattern practical and maintainable.
Practical Checklist Before Using Strategy Pattern
Before using Strategy Pattern, developers can ask these questions:
Do I have multiple ways to perform the same task?
Is the current code full of if or switch statements?
Will new behaviors be added later?
Should behavior be selected at runtime?
Can each behavior be tested separately?
Will an interface make the design cleaner?
Is the extra structure worth the flexibility?
If the answer is yes to several of these questions, Strategy Pattern may be a good design choice.
Conclusion
The Strategy Pattern is a behavioral design pattern that allows developers to define interchangeable algorithms or behaviors and switch between them without modifying the main application code. It is especially useful for payment methods, discount rules, shipping calculations, export formats, validation rules, and other behavior variations.
Strategy Pattern improves code organization by replacing large conditional blocks with focused strategy classes. It supports polymorphism, dependency injection, clean code, and the Open Closed Principle.
However, Strategy Pattern should be used only when behavior variation is real and meaningful. For simple logic, it may add unnecessary complexity. When applied correctly, the Strategy Pattern is one of the most powerful tools for building flexible, maintainable, and scalable object-oriented software.

