1. Hook & Problem Statement You're a Laravel developer. You need to build a shipping cost calculator. You start simple. if ($country === 'USA') { return 10; } Then the business adds: "We need different pricing for express shipping." So you add elseif ($shipping === 'express'). Then: "Different prices for heavy items." Another elseif. Then: "Discounts for prime members." Another condition. Then: "Free shipping for orders over $50." Another one. Before you know it, your method looks like this: public function calculateShipping($order) { if ($order->country === 'USA') { if ($order->weight > 20) { if ($order->shipping_method === 'express') { // 10 lines of complex logic } else { // 8 lines of complex logic } } else { // More nested conditions... } } elseif ($order->country === 'Canada') { // 20 more lines } elseif ($order->country === 'UK') { // 20 more lines } else { // 20 more lines } } Your function is now 200 lines long. It's unreadable. It's untestable. Adding a new shipping method means understanding the entire mess. This is the Conditional Apocalypse. And it's the problem the Strategy Pattern was designed to solve. 2. Why This Pattern Exists The Software Engineering Problem It Solves The Strategy Pattern solves the problem of having multiple algorithms or behaviors that need to be interchangeable at runtime. In any non-trivial application, you'll have variations of the same behavior: Different payment gateways (Stripe, PayPal, etc.) Different shipping calculators (USPS, FedEx, DHL) Different notification channels (Email, SMS, Slack) Different discount calculations (Percentage, Fixed amount, Bulk discount) The naive approach—using conditional logic—creates tight coupling, code duplication, and violates the Open/Closed Principle. The Pain That Existed Before Before the Strategy Pattern (or in codebases that don't use it), developers suffered from: Massive if-else/switch blocks that grew endlessly with every new requirement. Duplicated code across different branches. Tight coupling between the context class and all variations. Difficult testing: Every branch needed separate test coverage. Open/Closed Principle violation: Adding a new behavior required modifying existing code. Low cohesion: A single class handled multiple, unrelated responsibilities. Why Large Applications Need It As your application grows, the number of variations increases. A simple shipping calculator might have: 5 countries 3 shipping methods 2 weight tiers 3 customer tiers 4 discount types That's 5 × 3 × 2 × 3 × 4 = 360 conditional combinations. Adding a new country (×5 = 1800 combinations) becomes a nightmare. The Strategy Pattern reduces this complexity by: Extracting each algorithm into its own class. Making algorithms interchangeable at runtime. Isolating changes to specific strategies. 3. Real World Analogy The GPS Navigation System Imagine you're driving to a destination. Your GPS offers multiple routes: Fastest Route: Prioritizes highways and speed. Shortest Route: Prioritizes distance. Scenic Route: Prioritizes beautiful roads. Eco Route: Prioritizes fuel efficiency. The Bad Way: Your GPS has a single massive function with if-else statements for each route type. Adding a new route (e.g., "Avoid Highways") requires modifying the core GPS logic. The Strategy Pattern Way: The GPS has a RouteStrategy interface with a calculateRoute() method. FastestRouteStrategy implements RouteStrategy ShortestRouteStrategy implements RouteStrategy ScenicRouteStrategy implements RouteStrategy EcoRouteStrategy implements RouteStrategy The GPS (the context) receives a RouteStrategy and delegates the calculation. Analogy Mapping: GPS System: The context class (e.g., ShippingCalculator). Route Strategy: The strategy interface. Fastest/Shortest/Scenic Route: Concrete strategies. Driver: The client code. Switching routes: Changing the strategy at runtime. Adding a new route: Creating a new strategy class. Why it works: The GPS doesn't need to know how each route is calculated. It just delegates. Adding a new route doesn't change the GPS logic. 4. The Pain (Bad Design) Let's look at a typical conditional nightmare. namespace App\Services; use App\Models\Order; class ShippingCalculator { private array $config; public function __construct() { $this->config = config('shipping'); } public function calculate(Order $order): float { $total = 0; $country = $order->shipping_country; $method = $order->shipping_method; $weight = $order->total_weight; // Base rates by country if ($country === 'USA') { if ($method === 'standard') { if ($weight <= 5) { $total = 5.00; } elseif ($weight <= 10) { $total = 8.50; } elseif ($weight <= 20) { $total = 12.00; } else { $total = 12.00 + (($weight - 20) * 0.50); } } elseif ($method === 'express') { if ($weight <= 5) { $total = 15.00; } elseif ($weight <= 10) { $total = 22.50; } elseif ($weight <= 20) { $total = 30.00; } else { $total = 30.00 + (($weight - 20) * 1.00); } } elseif ($method === 'overnight') { if ($weight <= 5) { $total = 25.00; } elseif ($weight <= 10) { $total = 35.00; } elseif ($weight <= 20) { $total = 45.00; } else { $total = 45.00 + (($weight - 20) * 1.50); } } } elseif ($country === 'Canada') { if ($method === 'standard') { if ($weight <= 5) { $total = 10.00; } elseif ($weight <= 10) { $total = 15.00; } elseif ($weight <= 20) { $total = 22.00; } else { $total = 22.00 + (($weight - 20) * 0.75); } } elseif ($method === 'express') { // More nested conditions... } // And so on... } elseif ($country === 'UK') { // Another 30 lines... } elseif ($country === 'Australia') { // Another 30 lines... } else { // International shipping... } // Apply discounts if ($order->subtotal > 100) { $total *= 0.90; // 10% discount } // Apply prime member discount if ($order->user && $order->user->is_prime) { $total *= 0.85; // 15% discount } // Add handling fee for heavy items if ($weight > 50) { $total += 10.00; } // Add insurance for valuable items if ($order->subtotal > 500) { $total += $order->subtotal * 0.02; } // Add weekend delivery surcharge if (now()->isWeekend() && $method === 'overnight') { $total += 10.00; } // Round and return return round($total, 2); } } Why This Is Terrible Massive Conditional Block: 100+ lines of nested if-else. Low Cohesion: The class handles US rates, Canadian rates, UK rates, Australian rates, international rates, discounts, prime member discounts, handling fees, insurance, and weekend surcharges. High Coupling: All logic is tightly coupled in one method. Difficult Testing: Testing all combinations requires 100+ test cases. Violates OCP: Adding a new country requires modifying this class. Violates SRP: This class has at least 10 responsibilities. Difficult to Read: No one can understand this method. Why Developers Write Code Like This We write code like this because: It's the "quick and dirty" solution. We don't think about future growth. We're not familiar with design patterns. We think "just one more if-else" won't hurt. We haven't learned to recognize the Strategy pattern opportunity. 5. Solution Overview The Strategy Pattern is a behavioral design pattern that enables selecting an algorithm's implementation at runtime. It defines a family of algorithms, encapsulates each one, and makes them interchangeable. Core Idea Instead of using conditional logic to choose between different behaviors, you: Define a common interface for all algorithms. Implement each algorithm in its own class. Inject the algorithm (strategy) into the context class. The context delegates the work to the strategy, which is interchangeable at runtime. Main Participants Strategy Interface: Defines a common method that all strategies must implement. Concrete Strategies: Different implementations of the algorithm. Context: The class that uses the strategy. It holds a reference to a strategy and delegates work to it. Client: The code that selects and injects the appropriate strategy. How Objects Collaborate Client → Context → Strategy Interface → Concrete Strategy A → Concrete Strategy B → Concrete Strategy C The client creates a concrete strategy and injects it into the context. The context uses the strategy without knowing which specific implementation it is. Mental Model Think of your car's gear shift. Strategy Interface: "Gear Selection" (the method you use). Concrete Strategies: Park, Reverse, Neutral, Drive, Low Gear. Context: The Car (uses the gear selection). Client: You (the driver). When you shift to "Drive," you're selecting a strategy. The car changes how it operates internally (different RPM, torque, etc.) without you knowing the details. Benefits Open/Closed: Add new strategies without changing existing code. Single Responsibility: Each strategy has one job. Testability: Test each strategy independently. Flexibility: Swap strategies at runtime. Readability: No massive conditionals. Trade-offs More Classes: Each strategy is a separate class. Complexity: More moving parts to understand. Indirection: The client must know which strategy to use. 6. UML Diagram Laravel Strategy Pattern Mermaid Diagram Diagram Explanation ShippingCalculator (Context) holds a reference to ShippingStrategy (Interface). Concrete Strategies implement the interface with specific algorithms. DiscountDecorator is a wrapper strategy that applies discounts to other strategies (decorator pattern). WeightBasedStrategy is a reusable strategy that calculates based on weight tiers. 7. Vanilla PHP Example Let's refactor the shipping calculator using the Strategy Pattern. Before Refactoring (The terrible conditional mess shown above) After Refactoring Step 1: Define the Strategy Interface interface ShippingStrategy { public function calculate(Order $order): float; } Step 2: Implement Concrete Strategies // Base strategy with common functionality abstract class BaseShippingStrategy implements ShippingStrategy { protected function getBaseRate(string $country, string $method, float $weight): float { $rates = [ 'USA' => [ 'standard' => [5.00, 8.50, 12.00, 12.00], 'express' => [15.00, 22.50, 30.00, 30.00], 'overnight' => [25.00, 35.00, 45.00, 45.00], ], 'Canada' => [ 'standard' => [10.00, 15.00, 22.00, 22.00], 'express' => [20.00, 28.00, 38.00, 38.00], 'overnight' => [35.00, 45.00, 55.00, 55.00], ], // More countries can be added ]; if (!isset($rates[$country][$method])) { throw new \Exception("Invalid country or method combination"); } $rateTable = $rates[$country][$method]; $weightTiers = [5, 10, 20]; for ($i = 0; $i < count($weightTiers); $i++) { if ($weight <= $weightTiers[$i]) { return $rateTable[$i]; } } // Over 20 lbs: base rate + per-pound charge $baseRate = $rateTable[3]; $excessWeight = $weight - 20; $perPoundCharge = match($method) { 'standard' => 0.50, 'express' => 1.00, 'overnight' => 1.50, default => 0.50, }; return $baseRate + ($excessWeight * $perPoundCharge); } protected function getWeekendSurcharge(string $method): float { if (now()->isWeekend() && $method === 'overnight') { return 10.00; } return 0.00; } protected function getHeavyItemSurcharge(float $weight): float { return $weight > 50 ? 10.00 : 0.00; } protected function getInsuranceSurcharge(float $subtotal): float { return $subtotal > 500 ? $subtotal * 0.02 : 0.00; } } // USA Strategy class USAStrategy extends BaseShippingStrategy { public function calculate(Order $order): float { $base = $this->getBaseRate('USA', $order->shipping_method, $order->total_weight); $weekend = $this->getWeekendSurcharge($order->shipping_method); $heavy = $this->getHeavyItemSurcharge($order->total_weight); $insurance = $this->getInsuranceSurcharge($order->subtotal); return round($base + $weekend + $heavy + $insurance, 2); } } // Canada Strategy class CanadaStrategy extends BaseShippingStrategy { public function calculate(Order $order): float { $base = $this->getBaseRate('Canada', $order->shipping_method, $order->total_weight); $weekend = $this->getWeekendSurcharge($order->shipping_method); $heavy = $this->getHeavyItemSurcharge($order->total_weight); $insurance = $this->getInsuranceSurcharge($order->subtotal); return round($base + $weekend + $heavy + $insurance, 2); } } // UK Strategy class UKStrategy extends BaseShippingStrategy { public function calculate(Order $order): float { // UK-specific logic $base = 15.00; if ($order->total_weight > 10) { $base += ($order->total_weight - 10) * 0.60; } return round($base, 2); } } // International Strategy class InternationalStrategy extends BaseShippingStrategy { public function calculate(Order $order): float { // International flat rate + weight $base = 25.00; if ($order->total_weight > 5) { $base += ($order->total_weight - 5) * 2.00; } return round($base, 2); } } // Discount Strategy (Decorator) class DiscountStrategy implements ShippingStrategy { private ShippingStrategy $wrapped; private float $discount; public function __construct(ShippingStrategy $wrapped, float $discount) { $this->wrapped = $wrapped; $this->discount = $discount; } public function calculate(Order $order): float { $base = $this->wrapped->calculate($order); return round($base * (1 - $this->discount), 2); } } Step 3: The Context class ShippingCalculator { private ShippingStrategy $strategy; public function __construct(ShippingStrategy $strategy) { $this->strategy = $strategy; } public function calculate(Order $order): float { return $this->strategy->calculate($order); } public function setStrategy(ShippingStrategy $strategy): void { $this->strategy = $strategy; } } // Strategy Factory (optional) class ShippingStrategyFactory { public static function create(Order $order): ShippingStrategy { $country = $order->shipping_country; // Select base strategy $strategy = match($country) { 'USA' => new USAStrategy(), 'Canada' => new CanadaStrategy(), 'UK' => new UKStrategy(), default => new InternationalStrategy(), }; // Apply discounts if ($order->subtotal > 100) { $strategy = new DiscountStrategy($strategy, 0.10); } if ($order->user && $order->user->is_prime) { $strategy = new DiscountStrategy($strategy, 0.15); } return $strategy; } } Step 4: Usage // Client code $order = Order::find(123); // Using the factory $strategy = ShippingStrategyFactory::create($order); $calculator = new ShippingCalculator($strategy); $shippingCost = $calculator->calculate($order); // Or manual selection if ($order->shipping_country === 'USA') { $calculator = new ShippingCalculator(new USAStrategy()); } else { $calculator = new ShippingCalculator(new InternationalStrategy()); } $shippingCost = $calculator->calculate($order); // Or runtime strategy change $calculator->setStrategy(new CanadaStrategy()); $shippingCost = $calculator->calculate($order); What We Improved No Conditionals: The context has no if-else blocks. Open/Closed: Add a new country by creating a new strategy class. Single Responsibility: Each strategy handles one country. Testability: Test each strategy independently. Reusability: Strategies can be composed (e.g., DiscountStrategy wraps other strategies). Readability: Each class is small and focused. Flexibility: Strategies can be swapped at runtime. 8. Laravel Internal Example Laravel uses the Strategy Pattern extensively. Let's look at some key examples. Authentication Guards Laravel's authentication system uses the Strategy Pattern to support different guard types. // Illuminate\Contracts\Auth\Guard (Strategy Interface) interface Guard { public function check(): bool; public function user(): ?Authenticatable; public function id(): ?int; public function validate(array $credentials = []): bool; public function setUser(Authenticatable $user): static; } // SessionGuard (Strategy Implementation) class SessionGuard implements Guard { // Session-based authentication } // TokenGuard (Strategy Implementation) class TokenGuard implements Guard { // Token-based authentication } // The Context: AuthManager class AuthManager { public function guard(?string $name = null): Guard { $name = $name ?: $this->getDefaultDriver(); return $this->guards[$name] ??= $this->resolve($name); } protected function resolve($name): Guard { // Creates the appropriate guard based on configuration $config = $this->getConfig($name); $driverMethod = 'create'.ucfirst($config['driver']).'Driver'; if (method_exists($this, $driverMethod)) { return $this->{$driverMethod}($config); } } } Why This Is Elegant: The interface defines the contract for all guards. Each guard implements the interface in its own way. AuthManager (context) delegates to the appropriate guard. Adding a new guard type doesn't affect existing code. Filesystem Drivers Laravel's Filesystem uses the Strategy Pattern to support different storage providers. // Illuminate\Contracts\Filesystem\Filesystem (Strategy Interface) interface Filesystem { public function exists(string $path): bool; public function get(string $path): string; public function put(string $path, $contents, $options = []): bool; public function delete(string|array $paths): bool; // ... more methods } // LocalFilesystem (Strategy Implementation) class LocalFilesystem implements Filesystem { // Local filesystem operations } // S3Filesystem (Strategy Implementation) class S3Filesystem implements Filesystem { // S3 operations } // The Context: FilesystemManager class FilesystemManager { public function disk(?string $name = null): Filesystem { $name = $name ?: $this->getDefaultDriver(); return $this->disks[$name] ??= $this->resolve($name); } protected function resolve(string $name): Filesystem { $config = $this->getConfig($name); return match($config['driver']) { 'local' => $this->createLocalDriver($config), 's3' => $this->createS3Driver($config), // More drivers... }; } } Why This Is Elegant: All storage operations are interchangeable. You can switch between local and S3 without changing code. Adding a new driver (e.g., Google Cloud) is straightforward. Queue Drivers Laravel's Queue system uses the Strategy Pattern. // Illuminate\Contracts\Queue\Queue (Strategy Interface) interface Queue { public function push($job, $data = '', $queue = null); public function later($delay, $job, $data = '', $queue = null); public function pop($queue = null); } // DatabaseQueue (Strategy Implementation) class DatabaseQueue implements Queue { // Database queue operations } // RedisQueue (Strategy Implementation) class RedisQueue implements Queue { // Redis queue operations } // SqsQueue (Strategy Implementation) class SqsQueue implements Queue { // SQS queue operations } Why This Is Elegant: The same Queue::push() works regardless of the driver. Switching drivers is a configuration change. Each driver handles its own specific logic. Notification Channels Laravel's Notifications use the Strategy Pattern for different channels. // Interface (implicit via method signatures) class Notification { public function via($notifiable): array { return ['mail', 'database']; } } // The Context: ChannelManager class ChannelManager { public function send($notifiable, $notification, $channels = null): void { $channels = $channels ?: $notification->via($notifiable); foreach ($channels as $channel) { // The strategy pattern in action $this->channel($channel)->send($notifiable, $notification); } } } Why This Is Elegant: Each channel (mail, database, slack, etc.) is a strategy. Adding a new channel only requires implementing the channel interface. The notification system doesn't care which channels are used. Validation Rules Laravel's validation system uses the Strategy Pattern for rules. // Rule interface interface Rule { public function passes($attribute, $value): bool; public function message(): string; } // Custom rules (strategies) class Uppercase implements Rule { public function passes($attribute, $value): bool { return strtoupper($value) === $value; } public function message(): string { return 'The :attribute must be uppercase.'; } } // Usage $request->validate([ 'name' => ['required', new Uppercase], ]); Why This Is Elegant: Each rule is a self-contained strategy. You can add custom rules without changing the validation system. Rules can be combined and reused. 9. Real Laravel Application Example Let's build a Discount Calculation System using the Strategy Pattern. Scenario Your e-commerce application needs to support multiple discount types: Percentage Discount: 10% off everything. Fixed Amount Discount: $20 off orders over $100. Bulk Discount: Buy 2 get 1 free. Seasonal Discount: Variable discounts based on season. Referral Discount: 15% off for referred customers. Implementation Step 1: Define the Strategy Interface // app/Contracts/DiscountStrategy.php namespace App\Contracts; use App\Models\Order; use App\Models\User; interface DiscountStrategy { public function calculate(Order $order): float; public function getDescription(): string; public function isApplicable(Order $order): bool; public function getPriority(): int; } Step 2: Implement Concrete Strategies // app/Services/Discounts/PercentageDiscount.php namespace App\Services\Discounts; use App\Contracts\DiscountStrategy; use App\Models\Order; class PercentageDiscount implements DiscountStrategy { private float $percentage; private string $description; public function __construct(float $percentage, string $description = 'Percentage Discount') { $this->percentage = $percentage; $this->description = $description; } public function calculate(Order $order): float { return ($order->subtotal * $this->percentage) / 100; } public function getDescription(): string { return $this->description . " ({$this->percentage}%)"; } public function isApplicable(Order $order): bool { return $order->subtotal > 0; } public function getPriority(): int { return 10; // Medium priority } } // app/Services/Discounts/FixedAmountDiscount.php namespace App\Services\Discounts; use App\Contracts\DiscountStrategy; use App\Models\Order; class FixedAmountDiscount implements DiscountStrategy { private float $amount; private float $threshold; private string $description; public function __construct(float $amount, float $threshold = 0, string $description = 'Fixed Amount Discount') { $this->amount = $amount; $this->threshold = $threshold; $this->description = $description; } public function calculate(Order $order): float { return min($this->amount, $order->subtotal); } public function getDescription(): string { return $this->description . " (\${$this->amount})"; } public function isApplicable(Order $order): bool { return $order->subtotal >= $this->threshold; } public function getPriority(): int { return 20; // Lower priority than percentage } } // app/Services/Discounts/BulkDiscount.php namespace App\Services\Discounts; use App\Contracts\DiscountStrategy; use App\Models\Order; class BulkDiscount implements DiscountStrategy { private int $minQuantity; private float $discountPercent; public function __construct(int $minQuantity = 2, float $discountPercent = 33.33) { $this->minQuantity = $minQuantity; $this->discountPercent = $discountPercent; } public function calculate(Order $order): float { $items = $order->items; $discount = 0; foreach ($items as $item) { if ($item['quantity'] >= $this->minQuantity) { // Buy 2 get 1 free = 33.33% discount $freeItems = floor($item['quantity'] / ($this->minQuantity + 1)); $discount += $freeItems * $item['price']; } } return $discount; } public function getDescription(): string { return "Buy " . ($this->minQuantity + 1) . " get 1 free"; } public function isApplicable(Order $order): bool { foreach ($order->items as $item) { if ($item['quantity'] >= $this->minQuantity + 1) { return true; } } return false; } public function getPriority(): int { return 5; // High priority } } // app/Services/Discounts/SeasonalDiscount.php namespace App\Services\Discounts; use App\Contracts\DiscountStrategy; use App\Models\Order; class SeasonalDiscount implements DiscountStrategy { private array $seasonalRates; public function __construct(array $seasonalRates) { $this->seasonalRates = $seasonalRates; } public function calculate(Order $order): float { $month = now()->month; $rate = $this->seasonalRates[$month] ?? 0; return ($order->subtotal * $rate) / 100; } public function getDescription(): string { $month = now()->month; $rate = $this->seasonalRates[$month] ?? 0; return "Seasonal Discount ({$rate}%)"; } public function isApplicable(Order $order): bool { $month = now()->month; return isset($this->seasonalRates[$month]) && $this->seasonalRates[$month] > 0; } public function getPriority(): int { return 15; // Medium priority } } // app/Services/Discounts/ReferralDiscount.php namespace App\Services\Discounts; use App\Contracts\DiscountStrategy; use App\Models\Order; class ReferralDiscount implements DiscountStrategy { private float $percentage; private string $description; public function __construct(float $percentage = 15) { $this->percentage = $percentage; $this->description = 'Referral Discount'; } public function calculate(Order $order): float { return ($order->subtotal * $this->percentage) / 100; } public function getDescription(): string { return $this->description . " ({$this->percentage}%)"; } public function isApplicable(Order $order): bool { return $order->user && $order->user->referred_by !== null; } public function getPriority(): int { return 5; // High priority } } Step 3: The Context (Discount Calculator) // app/Services/DiscountCalculator.php namespace App\Services; use App\Contracts\DiscountStrategy; use App\Models\Order; use Illuminate\Support\Collection; class DiscountCalculator { private Collection $strategies; public function __construct(array $strategies = []) { $this->strategies = collect($strategies); } public function addStrategy(DiscountStrategy $strategy): self { $this->strategies->push($strategy); return $this; } public function getApplicableStrategies(Order $order): Collection { return $this->strategies ->filter(fn(DiscountStrategy $strategy) => $strategy->isApplicable($order)) ->sortBy(fn(DiscountStrategy $strategy) => $strategy->getPriority()); } public function calculate(Order $order): array { $applicable = $this->getApplicableStrategies($order); if ($applicable->isEmpty()) { return [ 'total_discount' => 0, 'applied_discounts' => [], 'final_subtotal' => $order->subtotal, ]; } $totalDiscount = 0; $appliedDiscounts = []; $currentSubtotal = $order->subtotal; foreach ($applicable as $strategy) { $discount = $strategy->calculate($order); // Cap discount at remaining subtotal $discount = min($discount, $currentSubtotal); if ($discount > 0) { $totalDiscount += $discount; $currentSubtotal -= $discount; $appliedDiscounts[] = [ 'description' => $strategy->getDescription(), 'amount' => $discount, 'remaining_subtotal' => $currentSubtotal, ]; } // Stop if no more subtotal to discount if ($currentSubtotal <= 0) { break; } } return [ 'total_discount' => round($totalDiscount, 2), 'applied_discounts' => $appliedDiscounts, 'final_subtotal' => round($currentSubtotal, 2), 'original_subtotal' => $order->subtotal, ]; } } Step 4: Service Provider // app/Providers/DiscountServiceProvider.php namespace App\Providers; use App\Contracts\DiscountStrategy; use App\Services\DiscountCalculator; use App\Services\Discounts\BulkDiscount; use App\Services\Discounts\FixedAmountDiscount; use App\Services\Discounts\PercentageDiscount; use App\Services\Discounts\ReferralDiscount; use App\Services\Discounts\SeasonalDiscount; use Illuminate\Support\ServiceProvider; class DiscountServiceProvider extends ServiceProvider { public function register(): void { // Register all discount strategies $this->app->tag([ PercentageDiscount::class, FixedAmountDiscount::class, BulkDiscount::class, SeasonalDiscount::class, ReferralDiscount::class, ], 'discount_strategies'); // Register the calculator with all strategies $this->app->singleton(DiscountCalculator::class, function ($app) { $calculator = new DiscountCalculator(); // Add strategies $calculator->addStrategy(new PercentageDiscount(10, 'Standard Discount')); $calculator->addStrategy(new FixedAmountDiscount(20, 100, 'Spend $100 get $20 off')); $calculator->addStrategy(new BulkDiscount(2, 33.33)); $calculator->addStrategy(new SeasonalDiscount([ 1 => 5, // January: 5% 2 => 0, // February: 0% 3 => 0, // March: 0% 4 => 0, // April: 0% 5 => 0, // May: 0% 6 => 0, // June: 0% 7 => 0, // July: 0% 8 => 0, // August: 0% 9 => 0, // September: 0% 10 => 0, // October: 0% 11 => 15, // November: 15% 12 => 20, // December: 20% ])); $calculator->addStrategy(new ReferralDiscount(15)); // You could also add strategies dynamically based on configuration $configuredStrategies = config('discounts.strategies', []); foreach ($configuredStrategies as $strategyClass) { if (class_exists($strategyClass)) { $calculator->addStrategy(app($strategyClass)); } } return $calculator; }); } } Step 5: Controller // app/Http/Controllers/CartController.php namespace App\Http\Controllers; use App\Models\Cart; use App\Services\DiscountCalculator; use Illuminate\Http\Request; class CartController extends Controller { public function __construct( private readonly DiscountCalculator $discountCalculator ) {} public function checkout(Request $request) { $cart = $request->user()->cart; // Convert cart to order structure for discount calculation $order = (object) [ 'subtotal' => $cart->subtotal, 'items' => $cart->items->toArray(), 'user' => $request->user(), ]; $result = $this->discountCalculator->calculate($order); return view('checkout', [ 'subtotal' => $order->subtotal, 'discounts' => $result['applied_discounts'], 'total_discount' => $result['total_discount'], 'final_total' => $result['final_subtotal'], ]); } } Why This Design Works Open/Closed: Add a new discount type by creating a new strategy class. Single Responsibility: Each discount type is in its own class. Testability: Test each discount strategy independently. Flexibility: Discounts can be enabled/disabled by adding/removing strategies. Composable: Strategies are applied in priority order. Readability: No massive conditional blocks. 10. SOLID Principles Mapping O - Open/Closed Principle (OCP) The Strategy Pattern is the poster child for OCP. You can add new strategies without modifying the context or existing strategies. // Adding a new discount type class NewYearDiscount implements DiscountStrategy { public function calculate(Order $order): float { // New Year special logic } } // No changes to DiscountCalculator required! S - Single Responsibility Principle (SRP) Each strategy has one responsibility: implementing a specific algorithm. PercentageDiscount: Calculates percentage discounts. FixedAmountDiscount: Calculates fixed discounts. BulkDiscount: Calculates bulk discounts. L - Liskov Substitution Principle (LSP) All strategies implement the same interface and are substitutable. function applyDiscount(DiscountStrategy $strategy, Order $order) { // Works with any strategy return $strategy->calculate($order); } D - Dependency Inversion Principle (DIP) The context (DiscountCalculator) depends on the DiscountStrategy interface, not on concrete implementations. I - Interface Segregation Principle (ISP) The interface is focused and minimal: interface DiscountStrategy { public function calculate(Order $order): float; public function getDescription(): string; public function isApplicable(Order $order): bool; public function getPriority(): int; } No strategy is forced to implement methods it doesn't need. 11. Trade-offs Benefits Flexibility: Add new algorithms without changing existing code. Testability: Test each strategy independently. Readability: No massive conditionals. Maintainability: Changes are isolated to specific strategies. Reusability: Strategies can be reused across different contexts. Runtime Behavior: Swap strategies at runtime. Costs More Classes: Each strategy is a separate class. Complexity: More moving parts. Indirection: The client must know which strategy to use. Over-engineering: Not every conditional needs the Strategy Pattern. When Is Complexity Justified? Use the Strategy Pattern when: You have multiple algorithms for the same task. The algorithms vary independently from the context. You need to be able to switch algorithms at runtime. The number of algorithms is likely to grow. Avoid the Strategy Pattern when: You have only one algorithm (YAGNI). The algorithms are trivial (one line of code). The logic is tightly coupled and unlikely to change. 12. When NOT To Use It 3 Green Flags (USE STRATEGY) Multiple Algorithms: You have 3+ ways to perform the same operation. Runtime Selection: You need to choose the algorithm at runtime. Likely to Grow: The number of algorithms will increase over time. 3 Red Flags (AVOID STRATEGY) Single Algorithm: You only have one way to perform the operation. Trivial Variation: The difference is a single configuration value. // BAD: Over-engineered strategy interface DiscountStrategy { public function calculate($order): float; } class TenPercentDiscount implements DiscountStrategy { /* ... */ } class TwentyPercentDiscount implements DiscountStrategy { /* ... */ } // GOOD: Simple configuration $discount = match($level) { 'basic' => 0.10, 'premium' => 0.20, default => 0, }; Tightly Coupled Algorithms: The algorithms share most of their logic. 13. Common Mistakes 1. Strategy Overload (Too Many Strategies) // BAD: Creating strategies for everything class AddOneStrategy implements CalculateStrategy { /* ... */ } class AddTwoStrategy implements CalculateStrategy { /* ... */ } // 100 strategies for every possible value Problem: You're creating strategies for trivial variations. Fix: Use configuration or simple conditionals for trivial variations. 2. Strategy Interface Bloated // BAD: Bloated interface interface Strategy { public function execute(): void; public function validate(): bool; public function getPriority(): int; public function getDescription(): string; public function getKey(): string; public function getConfig(): array; public function canExecute(): bool; public function onSuccess(): void; public function onFailure(): void; public function rollback(): void; } Problem: Many strategies don't need all these methods. Fix: Keep the interface focused. Use composition for optional behaviors. 3. Strategy Selection Logic in Client // BAD: Client decides which strategy if ($order->country === 'USA') { $strategy = new USAStrategy(); } elseif ($order->country === 'Canada') { $strategy = new CanadaStrategy(); } else { $strategy = new InternationalStrategy(); } Problem: The strategy selection logic is still in the client. Fix: Use a factory or resolver to encapsulate the selection logic. 4. Using Strategy for Simple Conditionals // BAD: Strategy for a simple switch interface NotificationStrategy { public function send(): void; } class EmailStrategy implements NotificationStrategy { /* ... */ } class SmsStrategy implements NotificationStrategy { /* ... */ } // GOOD: Simple config $channels = match($preference) { 'email' => new EmailChannel(), 'sms' => new SmsChannel(), default => new DefaultChannel(), }; Problem: You're over-engineering simple logic. Fix: Use the Strategy Pattern only when the logic is complex or likely to grow. 5. Not Handling Strategy Selection // BAD: Null pointer when no strategy selected class ShippingCalculator { private ?ShippingStrategy $strategy; public function calculate(Order $order): float { return $this->strategy->calculate($order); // Null if not set! } } Problem: The strategy might be null. Fix: Set a default strategy or handle the null case. public function calculate(Order $order): float { if (!$this->strategy) { $this->strategy = new DefaultStrategy(); } return $this->strategy->calculate($order); } 14. Frequently Asked Interview Questions Beginner/Intermediate Q: What is the Strategy Pattern? A: A behavioral design pattern that enables selecting an algorithm's implementation at runtime by defining a family of algorithms and making them interchangeable. Q: What are the main participants in the Strategy Pattern? A: The Strategy Interface, Concrete Strategies, and the Context class. Q: How does the Strategy Pattern differ from the Factory Pattern? A: The Factory Pattern creates objects. The Strategy Pattern defines algorithms and makes them interchangeable. Q: When would you use the Strategy Pattern instead of if-else statements? A: When you have multiple algorithms for the same task, the algorithms are complex, and the number is likely to grow. Q: How does Laravel use the Strategy Pattern? A: In authentication guards, filesystem drivers, queue drivers, notification channels, and validation rules. Senior/Architect Q: Explain the difference between the Strategy Pattern and the State Pattern. A: Both use composition, but they solve different problems. Strategy is about algorithms that are interchangeable. State is about behavior that changes based on state, and the state itself manages the transitions. Q: How do you handle strategy selection in a large application? A: Use a Strategy Factory or Resolver that encapsulates the selection logic. The selection can be based on configuration, request parameters, or the object's state. Q: What's the relationship between the Strategy Pattern and the Dependency Inversion Principle? A: The Strategy Pattern is an implementation of DIP. The context depends on the abstraction (Strategy Interface), not on concrete strategies. Q: How do you test a class that uses the Strategy Pattern? A: You can test the context with mock strategies. You can test each strategy independently. You can test the selection logic (factory) separately. Q: What's the performance impact of using the Strategy Pattern? A: Minimal. There's a slight overhead from method calls and object creation, but it's negligible compared to the benefits of maintainability and flexibility. 15. Interactive Practice Challenge The Requirement You're building a Notification Delivery System for your SaaS application. The current code uses massive conditionals to handle different notification channels. The Code (POOR DESIGN) // app/Services/NotificationService.php namespace App\Services; use App\Models\Notification; use App\Models\User; use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Http; use Twilio\Rest\Client; class NotificationService { private array $config; public function __construct() { $this->config = config('services'); } public function send(Notification $notification, User $user): array { $results = []; $channels = $user->notification_preferences ?? ['email']; foreach ($channels as $channel) { if ($channel === 'email') { try { Mail::to($user->email)->send(new \App\Mail\NotificationMessage($notification)); $results['email'] = ['success' => true]; Log::info('Email sent', ['user' => $user->id, 'notification' => $notification->id]); } catch (\Exception $e) { $results['email'] = ['success' => false, 'error' => $e->getMessage()]; Log::error('Email failed', ['error' => $e->getMessage()]); } } elseif ($channel === 'sms') { try { $twilio = new Client( config('services.twilio.sid'), config('services.twilio.token') ); $twilio->messages->create( $user->phone, [ 'from' => config('services.twilio.from'), 'body' => $notification->content, ] ); $results['sms'] = ['success' => true]; Log::info('SMS sent', ['user' => $user->id, 'notification' => $notification->id]); } catch (\Exception $e) { $results['sms'] = ['success' => false, 'error' => $e->getMessage()]; Log::error('SMS failed', ['error' => $e->getMessage()]); } } elseif ($channel === 'slack') { try { Http::post(config('services.slack.webhook_url'), [ 'text' => $notification->content, 'channel' => $user->slack_channel, ]); $results['slack'] = ['success' => true]; Log::info('Slack sent', ['user' => $user->id, 'notification' => $notification->id]); } catch (\Exception $e) { $results['slack'] = ['success' => false, 'error' => $e->getMessage()]; Log::error('Slack failed', ['error' => $e->getMessage()]); } } elseif ($channel === 'push') { try { $pushService = new \App\Services\PushService(); $pushService->send($user->device_token, $notification->title, $notification->content); $results['push'] = ['success' => true]; } catch (\Exception $e) { $results['push'] = ['success' => false, 'error' => $e->getMessage()]; } } elseif ($channel === 'webhook') { try { Http::post($user->webhook_url, [ 'event' => 'notification', 'data' => [ 'id' => $notification->id, 'title' => $notification->title, 'content' => $notification->content, 'user_id' => $user->id, ], ]); $results['webhook'] = ['success' => true]; } catch (\Exception $e) { $results['webhook'] = ['success' => false, 'error' => $e->getMessage()]; } } else { Log::warning('Unknown channel: ' . $channel); } } return $results; } } The Challenges New requirements are piling up: "We need to add Telegram channel." "We need to add Microsoft Teams channel." "We need to add WhatsApp channel." "We need to add push notifications for mobile apps." "We need to implement rate limiting per channel." "We need to retry failed notifications with exponential backoff." "We need to track notification delivery status in the database." "We need to send notifications in bulk with batching." The current code is already unmaintainable. Adding more channels will make it impossible. Your Task Refactor this system using the Strategy Pattern. Specifically: Define a NotificationStrategy interface with methods like send(), supports(), getRateLimit(), getRetryDelay(). Create strategy implementations for each channel: EmailStrategy SmsStrategy SlackStrategy PushStrategy WebhookStrategy Add TelegramStrategy, TeamsStrategy, WhatsAppStrategy as new channels. Implement a NotificationManager (context) that: Holds a collection of strategies. Delegates sending to the appropriate strategies. Handles batching and rate limiting. Implements retry logic with exponential backoff. Implement a StrategyFactory or StrategyResolver that selects strategies based on user preferences. Add logging and tracking without duplicating logic across strategies. Questions to Consider How should you handle shared logic (logging, tracking) across strategies? How do you implement rate limiting without duplicating code? How do you handle retries? Should it be in the strategy or the context? How do you implement batching for bulk notifications? How do you test each strategy independently? How do you handle strategies that require different configuration? (We won't provide the solution—refactor this code and master the Strategy Pattern!) 16. Final Mental Model To keep it simple, memorize these three sentences: One-sentence definition: The Strategy Pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable at runtime. One-sentence intuition: Instead of writing if-else for every behavior, encapsulate each behavior in its own class and inject the one you need. One-sentence decision rule: If you have multiple algorithms for the same task and the number is likely to grow, use the Strategy Pattern to avoid conditional hell. 17. Related Concepts SOLID Principles Open/Closed: Strategy is the practical implementation of OCP. Single Responsibility: Each strategy has one job. Liskov Substitution: All strategies are substitutable. Dependency Inversion: Context depends on strategy interface. Interface Segregation: Strategy interfaces are focused. Design Patterns Factory Pattern: Often used with Strategy to create strategies. State Pattern: Similar structure, different intent (state vs algorithm). Command Pattern: Encapsulates a request as an object. Decorator Pattern: Can wrap strategies to add behavior. Chain of Responsibility: Similar but passes through a chain. Laravel Internals Authentication Guards: Strategy pattern for auth types. Filesystem Drivers: Strategy pattern for storage types. Queue Drivers: Strategy pattern for queue types. Notification Channels: Strategy pattern for notification types. Validation Rules: Strategy pattern for validation types. Enterprise Patterns Strategy Pattern: The enterprise pattern itself. Policy Pattern: Similar concept in enterprise systems. Rule Pattern: For business rules that vary. Algorithm Pattern: For interchangeable algorithms. Final Thoughts The Strategy Pattern is one of the most powerful tools in your OOP toolbox. It's the antidote to the Conditional Apocalypse. It's the pattern that turns massive if-else blocks into clean, extensible, and testable code. Every time you find yourself writing a long if-else or switch statement, ask yourself: "Is this an algorithm that could be extracted into a strategy?" Remember: The Strategy Pattern isn't about avoiding conditionals entirely. It's about moving the conditionals to the right place (the factory or resolver) and encapsulating the algorithms in their own classes. Github: Strategy Pattern Practice Labs