← Back to Dashboard

Strategy Pattern

Laravel: Auth guards, Queue drivers, Cache stores

Strategy Pattern

Intent (GoF)

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

In plain language: when you have multiple ways of doing the same thing -- different calculation formulas, different sorting methods, different pricing rules -- you pull each one into its own class behind a shared interface. The class that needs the algorithm does not contain it; instead, it holds a reference to whichever strategy is currently in use. You can swap strategies at runtime without changing the class that uses them.

Real-World Analogy

Imagine a logistics company that ships parcels. When a customer places an order, they choose a shipping method: Standard, Express, or Overnight. Each method uses a different formula to calculate cost based on the package weight and delivery distance. The warehouse does not care how the cost is calculated -- it just asks the selected shipping strategy: "How much does this shipment cost?" If the company adds a "Same Day" option next month, the warehouse process does not change at all. A new strategy class is plugged in, and the system keeps working.

The Anti-Pattern: Giant Conditional Blocks

Without the Strategy pattern, you see calculation logic crammed into conditional branches:

class ShippingCalculator
{
    public function calculate(string $method, float $weight, float $distance): float
    {
        if ($method === 'standard') {
            return $weight * 0.5 + $distance * 0.1;
        } elseif ($method === 'express') {
            return $weight * 1.5 + $distance * 0.3 + 10.0;
        } elseif ($method === 'overnight') {
            return $weight * 3.0 + $distance * 0.5 + 25.0;
        } else {
            throw new \InvalidArgumentException("Unknown method: {$method}");
        }
    }
}

Problems:

  1. Single Responsibility violation. The ShippingCalculator knows the formulas for every shipping method. When the express pricing changes, you modify this class. When a new method is added, you modify this class. Every change to any method risks breaking others.
  2. Open/Closed Principle violation. Adding "Same Day" shipping means opening the class and adding another elseif. You cannot extend behaviour without modifying existing code.
  3. Testing difficulty. You cannot test Standard shipping in isolation from Express or Overnight. They are tangled in the same method.
  4. Code bloat. As methods multiply, the conditional block grows. In real systems, each branch might be 50-100 lines of complex calculation logic, not a clean one-liner.

The Fix: Strategy Pattern

Encapsulate each algorithm behind an interface:

interface ShippingStrategyInterface
{
    public function calculate(float $weight, float $distance): float;
    public function getName(): string;
}

class StandardShipping implements ShippingStrategyInterface
{
    public function calculate(float $weight, float $distance): float
    {
        return $weight * 0.5 + $distance * 0.1;
    }

    public function getName(): string
    {
        return 'Standard';
    }
}

class ExpressShipping implements ShippingStrategyInterface
{
    public function calculate(float $weight, float $distance): float
    {
        return $weight * 1.5 + $distance * 0.3 + 10.0;
    }

    public function getName(): string
    {
        return 'Express';
    }
}

class OvernightShipping implements ShippingStrategyInterface
{
    public function calculate(float $weight, float $distance): float
    {
        return $weight * 3.0 + $distance * 0.5 + 25.0;
    }

    public function getName(): string
    {
        return 'Overnight';
    }
}

The calculator delegates to whichever strategy it is given:

class ShippingCalculator
{
    private ShippingStrategyInterface $strategy;

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

    public function calculate(float $weight, float $distance): float
    {
        return $this->strategy->calculate($weight, $distance);
    }
}

Usage:

$calculator = new ShippingCalculator();

$calculator->setStrategy(new StandardShipping());
echo $calculator->calculate(10.0, 100.0); // 15.0

$calculator->setStrategy(new ExpressShipping());
echo $calculator->calculate(10.0, 100.0); // 55.0

What you gain:

  1. Each algorithm is isolated. Change the express formula without touching standard or overnight. Test each strategy independently.
  2. Open for extension. Adding "Same Day" shipping means creating a new class. No existing code is modified.
  3. Runtime flexibility. The strategy can be swapped at runtime based on user input, configuration, or business rules.
  4. Clean, readable code. The calculator class is tiny. Each strategy class is focused and self-contained.

Strategy vs. Factory: What Is the Difference?

This is a common point of confusion because both patterns involve interfaces and multiple implementations.

  • Factory answers: "Which object should I create?" It makes a one-time decision and returns the right implementation.
  • Strategy answers: "Which behaviour should I use right now?" It encapsulates interchangeable algorithms and allows swapping them, potentially multiple times during the lifetime of the host object.

In practice, you often use both together: a factory creates the right strategy, and the host object uses it. They are complementary, not competing patterns.

When to Use the Strategy Pattern

  • When you have multiple algorithms for the same task and need to choose between them at runtime
  • When a class has a long conditional block that selects behaviour based on a type or flag
  • When you want to isolate algorithm-specific data and behaviour from the class that uses it
  • When you need to test algorithms independently

When NOT to Use It

  • When there is genuinely only one algorithm with no variation
  • When the "strategies" are so simple (one line each) that separate classes would be over-engineering
  • When the algorithm never changes at runtime -- consider just injecting the right implementation via the constructor

Key Takeaway

If you have a method with a multi-branch conditional where each branch implements a different version of the same operation, you have strategies hiding inside a monolith. Extract them into their own classes, give them a common interface, and let the host object delegate to whichever one is appropriate.

What You Learned

  • The Strategy pattern lets you swap algorithms at runtime by encapsulating each one behind a common interface
  • A context class holds a reference to the current strategy and delegates the calculation to it
  • New strategies can be added without modifying existing code — just create a new class that implements the interface
  • Strategy is ideal whenever you have multiple ways to perform the same operation (pricing, sorting, validation)

Strategy Pattern in Laravel

The Strategy pattern is deeply embedded in Laravel's architecture. Every time you switch a cache driver, change an authentication guard, or swap a queue connection, you are using strategies. Laravel's configuration-driven approach makes strategy selection seamless.

Auth Guards: Strategies for Authentication

Laravel's authentication system uses the Strategy pattern to support multiple ways of identifying a user:

// config/auth.php
'guards' => [
    'web' => [
        'driver'   => 'session',       // SessionGuard strategy
        'provider' => 'users',
    ],
    'api' => [
        'driver'   => 'token',         // TokenGuard strategy
        'provider' => 'users',
    ],
],

Both SessionGuard and TokenGuard implement the Guard interface:

interface Guard
{
    public function check(): bool;
    public function guest(): bool;
    public function user(): ?Authenticatable;
    public function id(): int|string|null;
    public function validate(array $credentials = []): bool;
    public function setUser(Authenticatable $user): void;
}

When your code calls Auth::check(), it delegates to whichever guard strategy is currently active. The controller does not know or care whether authentication happens via session cookies or API tokens:

class DashboardController extends Controller
{
    public function index()
    {
        // This works identically whether the guard is session or token.
        // The strategy is selected by configuration, not by this code.
        if (Auth::check()) {
            $user = Auth::user();
            // ...
        }
    }
}

Cache Stores: Strategies for Data Caching

Each cache store is a strategy that implements the same caching interface but uses a different backend:

// config/cache.php
'stores' => [
    'file'      => ['driver' => 'file', 'path' => storage_path('framework/cache')],
    'redis'     => ['driver' => 'redis', 'connection' => 'cache'],
    'memcached' => ['driver' => 'memcached', 'servers' => [...]],
    'array'     => ['driver' => 'array', 'serialize' => false],
],

All stores implement the same Store contract:

interface Store
{
    public function get(string $key): mixed;
    public function put(string $key, mixed $value, int $seconds): bool;
    public function forget(string $key): bool;
    // ...
}

Your application code uses Cache::get() and Cache::put() regardless of whether the data lives in a file, Redis, or Memcached. The strategy can be changed in configuration without touching any application code:

// This code works identically with file, redis, or memcached
Cache::put('report_data', $reportData, 3600);
$data = Cache::get('report_data');

Queue Drivers: Strategies for Job Processing

Queue connections follow the same pattern:

// config/queue.php
'connections' => [
    'sync'     => ['driver' => 'sync'],
    'database' => ['driver' => 'database', 'table' => 'jobs'],
    'redis'    => ['driver' => 'redis', 'connection' => 'default'],
    'sqs'      => ['driver' => 'sqs', 'key' => '...', 'secret' => '...'],
],

Each driver is a strategy for processing queued jobs. During local development you might use sync (process immediately). In staging you use database. In production you use redis or sqs. Your job dispatch code never changes:

// Identical regardless of queue driver
ProcessPayment::dispatch($payment);

Implementing Your Own Strategy in Laravel

Here is how you might implement a pricing strategy system in a Laravel application:

// The strategy interface
interface PricingStrategyInterface
{
    public function calculateDiscount(float $subtotal): float;
    public function getName(): string;
}

// Concrete strategies
class RegularPricing implements PricingStrategyInterface
{
    public function calculateDiscount(float $subtotal): float
    {
        return 0.0; // No discount
    }

    public function getName(): string
    {
        return 'Regular';
    }
}

class MemberPricing implements PricingStrategyInterface
{
    public function calculateDiscount(float $subtotal): float
    {
        return $subtotal * 0.10; // 10% member discount
    }

    public function getName(): string
    {
        return 'Member';
    }
}

class PromoPricing implements PricingStrategyInterface
{
    public function calculateDiscount(float $subtotal): float
    {
        return $subtotal * 0.25; // 25% promo discount
    }

    public function getName(): string
    {
        return 'Promo';
    }
}

// The context class that uses a strategy
class OrderCalculator
{
    public function __construct(
        private PricingStrategyInterface $pricingStrategy,
    ) {}

    public function setPricingStrategy(PricingStrategyInterface $strategy): void
    {
        $this->pricingStrategy = $strategy;
    }

    public function calculateTotal(float $subtotal): float
    {
        $discount = $this->pricingStrategy->calculateDiscount($subtotal);
        return round($subtotal - $discount, 2);
    }
}

You can bind the appropriate strategy via Laravel's service container:

// In a service provider
$this->app->bind(PricingStrategyInterface::class, function ($app) {
    $type = config('pricing.strategy', 'regular');

    return match ($type) {
        'member' => new MemberPricing(),
        'promo'  => new PromoPricing(),
        default  => new RegularPricing(),
    };
});

The Common Thread

Auth guards, cache stores, queue drivers, mail transports, filesystem disks -- all of these are strategy implementations selected by configuration. The beauty of this approach is that your application logic remains unchanged regardless of which concrete strategy is active. When requirements change, you swap the strategy (often just by editing a config value), not the code that uses it.

This example is pre-filled and runnable. Click "Run" to see the output. Feel free to modify the code and experiment.

Output

                

            

Complete the code below to pass all test cases. Use the hints if you get stuck.