Strategy Design Pattern in Laravel: Complete Guide 2025



This content originally appeared on DEV Community and was authored by arasosman

Introduction

Are you tired of writing endless if-else statements or massive switch cases in your Laravel applications? Do you find yourself duplicating code across multiple controllers just to handle different business logic scenarios? The Strategy Design Pattern might be exactly what you need to clean up your codebase and make it more maintainable.

The Strategy pattern is one of the most powerful behavioral design patterns that allows you to define a family of algorithms, encapsulate each one, and make them interchangeable at runtime. In Laravel applications, this pattern can dramatically improve code organization, reduce complexity, and enhance testability.

In this comprehensive guide, you’ll learn how to implement the Strategy pattern in Laravel, explore real-world use cases, and discover advanced techniques that will transform how you structure your application logic. Whether you’re dealing with payment processing, notification systems, or complex business rules, this pattern will help you write cleaner, more flexible code.

What is the Strategy Design Pattern?

The Strategy Design Pattern is a behavioral design pattern that enables selecting algorithms at runtime. Instead of implementing a single algorithm directly, code receives run-time instructions as to which in a family of algorithms to use.

Core Components of Strategy Pattern

The Strategy pattern consists of three main components:

  1. Strategy Interface: Defines the contract that all concrete strategies must implement
  2. Concrete Strategies: Specific implementations of the strategy interface
  3. Context: The class that uses a strategy and can switch between different strategies

Benefits in Laravel Applications

  • Eliminates complex conditional logic: Replace lengthy if-else chains with clean, organized classes
  • Improves testability: Each strategy can be tested independently
  • Enhances maintainability: Adding new strategies doesn’t require modifying existing code
  • Follows SOLID principles: Particularly the Open/Closed Principle
  • Increases code reusability: Strategies can be used across different contexts

Basic Implementation in Laravel

Let’s start with a simple example of implementing the Strategy pattern for handling different payment methods in an e-commerce application.

Step 1: Create the Strategy Interface

<?php

namespace App\Strategies\Payment;

interface PaymentStrategyInterface
{
    public function pay(float $amount, array $paymentData): array;
    public function validate(array $paymentData): bool;
    public function getPaymentMethod(): string;
}

Step 2: Implement Concrete Strategies

<?php

namespace App\Strategies\Payment;

class CreditCardPaymentStrategy implements PaymentStrategyInterface
{
    public function pay(float $amount, array $paymentData): array
    {
        // Simulate credit card processing
        $transactionId = 'cc_' . uniqid();

        // Process payment logic here
        $success = $this->processCreditCardPayment($amount, $paymentData);

        return [
            'success' => $success,
            'transaction_id' => $transactionId,
            'payment_method' => $this->getPaymentMethod(),
            'amount' => $amount
        ];
    }

    public function validate(array $paymentData): bool
    {
        return isset($paymentData['card_number']) && 
               isset($paymentData['cvv']) && 
               isset($paymentData['expiry_date']);
    }

    public function getPaymentMethod(): string
    {
        return 'credit_card';
    }

    private function processCreditCardPayment(float $amount, array $paymentData): bool
    {
        // Integration with payment gateway
        return true; // Simplified for example
    }
}
<?php

namespace App\Strategies\Payment;

class PayPalPaymentStrategy implements PaymentStrategyInterface
{
    public function pay(float $amount, array $paymentData): array
    {
        $transactionId = 'pp_' . uniqid();

        $success = $this->processPayPalPayment($amount, $paymentData);

        return [
            'success' => $success,
            'transaction_id' => $transactionId,
            'payment_method' => $this->getPaymentMethod(),
            'amount' => $amount
        ];
    }

    public function validate(array $paymentData): bool
    {
        return isset($paymentData['email']) && 
               isset($paymentData['password']);
    }

    public function getPaymentMethod(): string
    {
        return 'paypal';
    }

    private function processPayPalPayment(float $amount, array $paymentData): bool
    {
        // PayPal API integration
        return true; // Simplified for example
    }
}

Step 3: Create the Context Class

<?php

namespace App\Services;

use App\Strategies\Payment\PaymentStrategyInterface;

class PaymentProcessor
{
    private PaymentStrategyInterface $strategy;

    public function __construct(PaymentStrategyInterface $strategy)
    {
        $this->strategy = $strategy;
    }

    public function setStrategy(PaymentStrategyInterface $strategy): void
    {
        $this->strategy = $strategy;
    }

    public function processPayment(float $amount, array $paymentData): array
    {
        if (!$this->strategy->validate($paymentData)) {
            throw new \InvalidArgumentException('Invalid payment data for ' . $this->strategy->getPaymentMethod());
        }

        return $this->strategy->pay($amount, $paymentData);
    }

    public function getPaymentMethod(): string
    {
        return $this->strategy->getPaymentMethod();
    }
}

Advanced Implementation Techniques

Using Laravel’s Service Container

Laravel’s service container makes it easy to manage strategy dependencies and inject them where needed.

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Strategies\Payment\CreditCardPaymentStrategy;
use App\Strategies\Payment\PayPalPaymentStrategy;
use App\Services\PaymentProcessor;

class PaymentServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->bind('payment.credit_card', CreditCardPaymentStrategy::class);
        $this->app->bind('payment.paypal', PayPalPaymentStrategy::class);

        $this->app->bind(PaymentProcessor::class, function ($app) {
            // Default to credit card strategy
            return new PaymentProcessor($app->make('payment.credit_card'));
        });
    }
}

Strategy Factory Pattern

Create a factory to dynamically select strategies based on runtime conditions:

<?php

namespace App\Factories;

use App\Strategies\Payment\PaymentStrategyInterface;
use App\Strategies\Payment\CreditCardPaymentStrategy;
use App\Strategies\Payment\PayPalPaymentStrategy;
use App\Strategies\Payment\BankTransferPaymentStrategy;

class PaymentStrategyFactory
{
    private array $strategies = [];

    public function __construct()
    {
        $this->strategies = [
            'credit_card' => CreditCardPaymentStrategy::class,
            'paypal' => PayPalPaymentStrategy::class,
            'bank_transfer' => BankTransferPaymentStrategy::class,
        ];
    }

    public function create(string $paymentMethod): PaymentStrategyInterface
    {
        if (!isset($this->strategies[$paymentMethod])) {
            throw new \InvalidArgumentException("Payment method '{$paymentMethod}' not supported");
        }

        $strategyClass = $this->strategies[$paymentMethod];
        return app($strategyClass);
    }

    public function getAvailableStrategies(): array
    {
        return array_keys($this->strategies);
    }
}

Using the Factory in Controllers

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Factories\PaymentStrategyFactory;
use App\Services\PaymentProcessor;

class PaymentController extends Controller
{
    private PaymentStrategyFactory $strategyFactory;

    public function __construct(PaymentStrategyFactory $strategyFactory)
    {
        $this->strategyFactory = $strategyFactory;
    }

    public function processPayment(Request $request)
    {
        $request->validate([
            'payment_method' => 'required|string',
            'amount' => 'required|numeric|min:0.01',
            'payment_data' => 'required|array'
        ]);

        try {
            $strategy = $this->strategyFactory->create($request->payment_method);
            $processor = new PaymentProcessor($strategy);

            $result = $processor->processPayment(
                $request->amount,
                $request->payment_data
            );

            return response()->json([
                'success' => true,
                'data' => $result
            ]);

        } catch (\Exception $e) {
            return response()->json([
                'success' => false,
                'message' => $e->getMessage()
            ], 400);
        }
    }
}

Real-World Use Cases

1. Notification System Strategy

Implement different notification channels (email, SMS, push notifications):

<?php

namespace App\Strategies\Notification;

interface NotificationStrategyInterface
{
    public function send(string $recipient, string $message, array $options = []): bool;
    public function getChannelName(): string;
}

class EmailNotificationStrategy implements NotificationStrategyInterface
{
    public function send(string $recipient, string $message, array $options = []): bool
    {
        // Send email using Laravel's Mail facade
        return Mail::to($recipient)->send(new GenericNotification($message, $options));
    }

    public function getChannelName(): string
    {
        return 'email';
    }
}

class SmsNotificationStrategy implements NotificationStrategyInterface
{
    public function send(string $recipient, string $message, array $options = []): bool
    {
        // Integrate with SMS service provider
        return true; // Simplified
    }

    public function getChannelName(): string
    {
        return 'sms';
    }
}

2. File Upload Strategy

Handle different file storage systems (local, S3, Google Cloud):

<?php

namespace App\Strategies\FileUpload;

interface FileUploadStrategyInterface
{
    public function upload($file, string $path): string;
    public function delete(string $path): bool;
    public function getUrl(string $path): string;
}

class S3UploadStrategy implements FileUploadStrategyInterface
{
    public function upload($file, string $path): string
    {
        $uploadPath = Storage::disk('s3')->put($path, $file);
        return $uploadPath;
    }

    public function delete(string $path): bool
    {
        return Storage::disk('s3')->delete($path);
    }

    public function getUrl(string $path): string
    {
        return Storage::disk('s3')->url($path);
    }
}

3. Pricing Strategy

Implement different pricing models for SaaS applications:

<?php

namespace App\Strategies\Pricing;

interface PricingStrategyInterface
{
    public function calculatePrice(array $usage): float;
    public function getPricingModel(): string;
}

class TieredPricingStrategy implements PricingStrategyInterface
{
    private array $tiers;

    public function __construct(array $tiers)
    {
        $this->tiers = $tiers;
    }

    public function calculatePrice(array $usage): float
    {
        $totalPrice = 0;
        $remainingUsage = $usage['quantity'];

        foreach ($this->tiers as $tier) {
            if ($remainingUsage <= 0) break;

            $tierUsage = min($remainingUsage, $tier['limit']);
            $totalPrice += $tierUsage * $tier['price'];
            $remainingUsage -= $tierUsage;
        }

        return $totalPrice;
    }

    public function getPricingModel(): string
    {
        return 'tiered';
    }
}

Testing Strategy Pattern Implementation

Unit Testing Individual Strategies

<?php

namespace Tests\Unit\Strategies\Payment;

use Tests\TestCase;
use App\Strategies\Payment\CreditCardPaymentStrategy;

class CreditCardPaymentStrategyTest extends TestCase
{
    private CreditCardPaymentStrategy $strategy;

    protected function setUp(): void
    {
        parent::setUp();
        $this->strategy = new CreditCardPaymentStrategy();
    }

    public function test_validates_credit_card_data()
    {
        $validData = [
            'card_number' => '4111111111111111',
            'cvv' => '123',
            'expiry_date' => '12/25'
        ];

        $this->assertTrue($this->strategy->validate($validData));
    }

    public function test_rejects_invalid_credit_card_data()
    {
        $invalidData = [
            'card_number' => '4111111111111111'
            // Missing cvv and expiry_date
        ];

        $this->assertFalse($this->strategy->validate($invalidData));
    }

    public function test_processes_payment_successfully()
    {
        $paymentData = [
            'card_number' => '4111111111111111',
            'cvv' => '123',
            'expiry_date' => '12/25'
        ];

        $result = $this->strategy->pay(100.00, $paymentData);

        $this->assertTrue($result['success']);
        $this->assertEquals('credit_card', $result['payment_method']);
        $this->assertEquals(100.00, $result['amount']);
        $this->assertArrayHasKey('transaction_id', $result);
    }
}

Integration Testing with Context

<?php

namespace Tests\Feature;

use Tests\TestCase;
use App\Services\PaymentProcessor;
use App\Strategies\Payment\CreditCardPaymentStrategy;
use App\Strategies\Payment\PayPalPaymentStrategy;

class PaymentProcessorTest extends TestCase
{
    public function test_processes_credit_card_payment()
    {
        $strategy = new CreditCardPaymentStrategy();
        $processor = new PaymentProcessor($strategy);

        $paymentData = [
            'card_number' => '4111111111111111',
            'cvv' => '123',
            'expiry_date' => '12/25'
        ];

        $result = $processor->processPayment(100.00, $paymentData);

        $this->assertTrue($result['success']);
        $this->assertEquals('credit_card', $result['payment_method']);
    }

    public function test_switches_between_strategies()
    {
        $processor = new PaymentProcessor(new CreditCardPaymentStrategy());

        $this->assertEquals('credit_card', $processor->getPaymentMethod());

        $processor->setStrategy(new PayPalPaymentStrategy());

        $this->assertEquals('paypal', $processor->getPaymentMethod());
    }
}

Performance Optimization and Best Practices

1. Lazy Loading Strategies

Only instantiate strategies when needed to improve performance:

<?php

namespace App\Services;

class OptimizedPaymentProcessor
{
    private array $strategies = [];
    private ?string $currentStrategy = null;

    public function setStrategy(string $strategyName): void
    {
        $this->currentStrategy = $strategyName;
    }

    public function processPayment(float $amount, array $paymentData): array
    {
        $strategy = $this->getStrategy($this->currentStrategy);

        if (!$strategy->validate($paymentData)) {
            throw new \InvalidArgumentException('Invalid payment data');
        }

        return $strategy->pay($amount, $paymentData);
    }

    private function getStrategy(string $strategyName): PaymentStrategyInterface
    {
        if (!isset($this->strategies[$strategyName])) {
            $this->strategies[$strategyName] = app("payment.{$strategyName}");
        }

        return $this->strategies[$strategyName];
    }
}

2. Caching Strategy Results

Cache expensive strategy operations:

<?php

namespace App\Strategies\Pricing;

use Illuminate\Support\Facades\Cache;

class CachedPricingStrategy implements PricingStrategyInterface
{
    private PricingStrategyInterface $strategy;
    private int $cacheTime;

    public function __construct(PricingStrategyInterface $strategy, int $cacheTime = 3600)
    {
        $this->strategy = $strategy;
        $this->cacheTime = $cacheTime;
    }

    public function calculatePrice(array $usage): float
    {
        $cacheKey = 'pricing:' . md5(serialize($usage));

        return Cache::remember($cacheKey, $this->cacheTime, function () use ($usage) {
            return $this->strategy->calculatePrice($usage);
        });
    }

    public function getPricingModel(): string
    {
        return $this->strategy->getPricingModel() . '_cached';
    }
}

3. Strategy Configuration

Use configuration files to manage strategy settings:

// config/strategies.php
return [
    'payment' => [
        'default' => 'credit_card',
        'strategies' => [
            'credit_card' => [
                'class' => \App\Strategies\Payment\CreditCardPaymentStrategy::class,
                'enabled' => true,
                'config' => [
                    'gateway' => 'stripe',
                    'test_mode' => env('PAYMENT_TEST_MODE', true)
                ]
            ],
            'paypal' => [
                'class' => \App\Strategies\Payment\PayPalPaymentStrategy::class,
                'enabled' => env('PAYPAL_ENABLED', false),
                'config' => [
                    'client_id' => env('PAYPAL_CLIENT_ID'),
                    'client_secret' => env('PAYPAL_CLIENT_SECRET')
                ]
            ]
        ]
    ]
];

Common Pitfalls and How to Avoid Them

1. Over-Engineering Simple Logic

Problem: Using the Strategy pattern for simple if-else logic that doesn’t need flexibility.

Solution: Only use the Strategy pattern when you have multiple algorithms that might change or be extended in the future.

2. Not Following the Interface Segregation Principle

Problem: Creating overly broad strategy interfaces that force implementations to provide unnecessary methods.

Solution: Keep strategy interfaces focused and specific to their purpose.

3. Poor Strategy Selection Logic

Problem: Complex or hardcoded strategy selection that defeats the pattern’s purpose.

Solution: Use factories or configuration-driven strategy selection.

4. Ignoring Dependencies

Problem: Strategies with complex dependencies that are hard to test and maintain.

Solution: Use dependency injection and keep strategies as simple as possible.

Advanced Patterns and Combinations

Chain of Responsibility with Strategy

Combine multiple patterns for complex scenarios:

<?php

namespace App\Services;

class PaymentChainProcessor
{
    private array $strategies = [];

    public function addStrategy(PaymentStrategyInterface $strategy, callable $condition): void
    {
        $this->strategies[] = ['strategy' => $strategy, 'condition' => $condition];
    }

    public function processPayment(float $amount, array $paymentData): array
    {
        foreach ($this->strategies as $item) {
            if (call_user_func($item['condition'], $amount, $paymentData)) {
                return $item['strategy']->pay($amount, $paymentData);
            }
        }

        throw new \Exception('No suitable payment strategy found');
    }
}

Observer Pattern Integration

Notify observers when strategies change:

<?php

namespace App\Services;

use Illuminate\Support\Facades\Event;

class ObservablePaymentProcessor extends PaymentProcessor
{
    public function setStrategy(PaymentStrategyInterface $strategy): void
    {
        $oldStrategy = $this->strategy ?? null;
        parent::setStrategy($strategy);

        Event::dispatch('payment.strategy.changed', [
            'old_strategy' => $oldStrategy?->getPaymentMethod(),
            'new_strategy' => $strategy->getPaymentMethod()
        ]);
    }
}

FAQ

What’s the difference between Strategy pattern and State pattern?

The Strategy pattern is about choosing different algorithms for the same task, while the State pattern is about changing behavior based on an object’s internal state. In Strategy, the client usually chooses which strategy to use, whereas in State, the state transitions happen automatically based on the object’s state.

When should I use the Strategy pattern over simple inheritance?

Use the Strategy pattern when you need to switch algorithms at runtime, when you have multiple ways to perform the same task, or when you want to avoid complex conditional statements. Inheritance is better when you have a clear “is-a” relationship and the behavior doesn’t need to change at runtime.

How do I handle strategy dependencies in Laravel?

Use Laravel’s service container to inject dependencies into your strategies. Bind your strategies in a service provider and let Laravel handle the dependency injection automatically. This makes your strategies more testable and maintainable.

Can I use the Strategy pattern with Laravel’s built-in features like Queue jobs?

Absolutely! You can implement different queuing strategies for different types of jobs, or use strategies within your queue jobs to handle different processing logic. The pattern works well with Laravel’s architecture.

How do I ensure my strategies are properly tested?

Test each strategy independently with unit tests, and test the context class with integration tests. Mock dependencies when necessary, and ensure you test both the happy path and error conditions for each strategy.

What’s the performance impact of using the Strategy pattern?

The performance impact is minimal if implemented correctly. Use lazy loading for strategies, cache expensive operations, and avoid creating unnecessary objects. The benefits of cleaner, more maintainable code usually outweigh any minor performance costs.

Conclusion

The Strategy Design Pattern is a powerful tool for creating flexible, maintainable Laravel applications. By encapsulating algorithms in separate classes and making them interchangeable, you can eliminate complex conditional logic, improve testability, and make your code more extensible.

Key takeaways from this guide:

  • Start simple: Begin with basic strategy implementations and add complexity only when needed
  • Use Laravel’s features: Leverage the service container, factories, and configuration for better strategy management
  • Test thoroughly: Write comprehensive tests for both individual strategies and their integration
  • Consider performance: Implement lazy loading and caching where appropriate
  • Avoid over-engineering: Only use the pattern when you truly need the flexibility it provides

The Strategy pattern works particularly well in Laravel because of the framework’s excellent dependency injection system and testing capabilities. Whether you’re building payment systems, notification services, or complex business logic, this pattern will help you create more organized and maintainable code.

Ready to implement the Strategy pattern in your Laravel application? Start with a simple use case, follow the examples in this guide, and gradually expand as your needs grow. Your future self (and your team) will thank you for the cleaner, more flexible codebase.

What’s your experience with design patterns in Laravel? Share your success stories or challenges in the comments below, and don’t forget to subscribe to our newsletter for more advanced Laravel development tips and techniques!


This content originally appeared on DEV Community and was authored by arasosman