Chaturmind
LearnDSASystem DesignInterview PrepDevOpsEngineering GrowthBlog
Start learning
Chaturmind

Structured learning paths for engineers who want to go deep. Written by practitioners.

Learn

  • Java
  • DSA
  • System Design
  • Spring Boot
  • AI / ML
  • DevOps
  • Engineering Growth
  • Java Interview Prep

Company

  • About
  • Blog
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Chaturmind. All rights reserved.

👥10,050visitors

Built for engineers who want to go deep.


← Low-Level Design & Design Patterns

OOP Fundamentals & SOLID Principles

  • Object-Oriented Design Refresher
  • Single Responsibility & Open/Closed
  • Liskov Substitution & Interface Segregation
  • Dependency Inversion

Creational Design Patterns

  • Singleton Pattern
  • Factory & Abstract Factory
  • Builder Pattern
  • Prototype Pattern

Structural Design Patterns

  • Adapter & Facade
  • Decorator Pattern
  • Composite & Proxy
  • Bridge & Flyweight

Behavioral Design Patterns

  • Strategy Pattern
  • Observer Pattern
  • Command Pattern
  • Template Method
  • State Pattern
  • Chain of Responsibility
  • Iterator & Mediator
  • Visitor Pattern

Machine Coding Exercises

  • Parking Lot — Requirements & Class Design
  • Parking Lot — Implementation
  • Elevator System — Requirements & Class Design
  • Elevator System — Implementation & Scheduling
  • Library Management System
  • Tic-Tac-Toe / Board Game Design
  • Chess Engine Design
  • Vending Machine Design
  • ATM System Design
  • In-Memory Rate Limiter
  • Thread-Safe LRU Cache
  • Connection Pool Design
  • Producer-Consumer Class Design
  • Movie Ticket Booking System
  • Hotel Reservation System
  • Airline Booking / Seat Selection System
  • Splitwise / Expense Sharing System
  • Design a Logging Framework
  • Design a Notification/Observer-Based Pub-Sub
  • Design a Cache with Pluggable Eviction Policy
  • Design Snake and Ladder
  • Design a Card Game Framework
  • Design an Inventory Management System
  • Restaurant Management System
  • Food Delivery Order Matching
  • Design a Shopping Cart & Checkout Flow
  • Design a Recommendation Engine
  • Design a Job Scheduler
  • Design a Circuit Breaker
  • Design a Distributed ID Generator
  • Design a Pub-Sub Message Broker
  • Design Twitter/X
  • Multi-Pattern Problem: Design a Ride Booking Class Model
  • Multi-Pattern Problem: Design a Food Ordering Class Model
  • Design an Authentication/Authorization Module
  • Design a Form/Survey Builder
  • Design a Meeting Room / Calendar Booking System
  • Design a Search/Filter Engine for an E-Commerce Catalog
  • Design a Voting/Polling System
  • Design a Retry Mechanism with Backoff
  • Design a Health Check Aggregator
  • Design a Feature Flag System
  • Design a Config Management Client
  • Design a Leader Election Algorithm
  • Design a Distributed Counter
  • Design a Bloom Filter
  • Design a Consistent Hashing Ring
  • Design a Distributed Tracing Library
  • Design a Metrics Collection Library
  • Design an Audit Logging Framework
  • Design a Plugin/Extension System
  • Design a Workflow Engine
  • Multi-Pattern Capstone: Design a Food Delivery App
Chaturmind
← Low-Level Design & Design Patterns

OOP Fundamentals & SOLID Principles

  • Object-Oriented Design Refresher
  • Single Responsibility & Open/Closed
  • Liskov Substitution & Interface Segregation
  • Dependency Inversion

Creational Design Patterns

  • Singleton Pattern
  • Factory & Abstract Factory
  • Builder Pattern
  • Prototype Pattern

Structural Design Patterns

  • Adapter & Facade
  • Decorator Pattern
  • Composite & Proxy
  • Bridge & Flyweight

Behavioral Design Patterns

  • Strategy Pattern
  • Observer Pattern
  • Command Pattern
  • Template Method
  • State Pattern
  • Chain of Responsibility
  • Iterator & Mediator
  • Visitor Pattern

Machine Coding Exercises

  • Parking Lot — Requirements & Class Design
  • Parking Lot — Implementation
  • Elevator System — Requirements & Class Design
  • Elevator System — Implementation & Scheduling
  • Library Management System
  • Tic-Tac-Toe / Board Game Design
  • Chess Engine Design
  • Vending Machine Design
  • ATM System Design
  • In-Memory Rate Limiter
  • Thread-Safe LRU Cache
  • Connection Pool Design
  • Producer-Consumer Class Design
  • Movie Ticket Booking System
  • Hotel Reservation System
  • Airline Booking / Seat Selection System
  • Splitwise / Expense Sharing System
  • Design a Logging Framework
  • Design a Notification/Observer-Based Pub-Sub
  • Design a Cache with Pluggable Eviction Policy
  • Design Snake and Ladder
  • Design a Card Game Framework
  • Design an Inventory Management System
  • Restaurant Management System
  • Food Delivery Order Matching
  • Design a Shopping Cart & Checkout Flow
  • Design a Recommendation Engine
  • Design a Job Scheduler
  • Design a Circuit Breaker
  • Design a Distributed ID Generator
  • Design a Pub-Sub Message Broker
  • Design Twitter/X
  • Multi-Pattern Problem: Design a Ride Booking Class Model
  • Multi-Pattern Problem: Design a Food Ordering Class Model
  • Design an Authentication/Authorization Module
  • Design a Form/Survey Builder
  • Design a Meeting Room / Calendar Booking System
  • Design a Search/Filter Engine for an E-Commerce Catalog
  • Design a Voting/Polling System
  • Design a Retry Mechanism with Backoff
  • Design a Health Check Aggregator
  • Design a Feature Flag System
  • Design a Config Management Client
  • Design a Leader Election Algorithm
  • Design a Distributed Counter
  • Design a Bloom Filter
  • Design a Consistent Hashing Ring
  • Design a Distributed Tracing Library
  • Design a Metrics Collection Library
  • Design an Audit Logging Framework
  • Design a Plugin/Extension System
  • Design a Workflow Engine
  • Multi-Pattern Capstone: Design a Food Delivery App
HomeLearnSystem DesignLow-Level Design & Design PatternsBehavioral Design Patterns
✓ FreeBeginner· 7 min read

Strategy Pattern

Encapsulating interchangeable algorithms behind a common interface, swappable at runtime — and the maintainability argument for choosing it over an if/else chain.

Published September 23, 2026


Strategy Pattern

The idea: swap the algorithm, not the caller

Strategy encapsulates a family of interchangeable algorithms behind one interface, so the code using them doesn't need to know or care which specific one is active.

interface PricingStrategy { double calculate(Order order); }

class RegularPricing implements PricingStrategy {
    public double calculate(Order order) { return order.subtotal(); }
}
class BulkDiscountPricing implements PricingStrategy {
    public double calculate(Order order) { return order.subtotal() * 0.9; }
}
class SeasonalPricing implements PricingStrategy {
    public double calculate(Order order) { return order.subtotal() * 0.85; }
}

class Checkout {
    private final PricingStrategy strategy; // injected — chosen by the caller, not hard-coded here
    Checkout(PricingStrategy strategy) { this.strategy = strategy; }
    double total(Order order) { return strategy.calculate(order); }
}
Checkout regular = new Checkout(new RegularPricing());
Checkout bulk = new Checkout(new BulkDiscountPricing());
// Same Checkout class, different pricing behavior — chosen at construction, swappable at runtime

Advertisement

Strategy vs if/else chains — the maintainability argument

// Without Strategy — every new pricing rule means editing this method
double total(Order order, String pricingType) {
    if (pricingType.equals("REGULAR")) return order.subtotal();
    else if (pricingType.equals("BULK")) return order.subtotal() * 0.9;
    else if (pricingType.equals("SEASONAL")) return order.subtotal() * 0.85;
    throw new IllegalArgumentException("Unknown pricing type");
}

This is the exact same OCP violation shown in Single Responsibility & Open/Closed's payment-method example: every new pricing rule means opening this method, adding a branch, and re-testing all existing branches for regression risk. With Strategy, adding a new pricing rule means writing one new class — the Checkout class (and anything else consuming PricingStrategy) never changes. This is the specific argument to give an interviewer who asks "why not just use if/else": it isn't about the if/else being slow, it's about every new case requiring a change to code that was already tested and shipped.

Design exercise: a SortingStrategy

interface SortStrategy { void sort(int[] data); }
class BubbleSort implements SortStrategy { public void sort(int[] data) { /* O(n^2), simple */ } }
class QuickSort implements SortStrategy { public void sort(int[] data) { /* O(n log n) average */ } }

class Sorter {
    private SortStrategy strategy;
    void setStrategy(SortStrategy strategy) { this.strategy = strategy; } // can even swap at runtime, not just construction
    void sort(int[] data) { strategy.sort(data); }
}

A Sorter could pick BubbleSort for tiny, nearly-sorted inputs and QuickSort for large ones — the point isn't that one algorithm is universally better, it's that the choice of algorithm is isolated from the code that needs sorting done.

Selecting strategies without an if/else chain

The if/else chain removed from Checkout tends to reappear wherever the strategy is chosen. Two clean ways to keep selection tidy:

1. A registry keyed by type (built by hand, or by Spring):

enum PricingType { REGULAR, BULK, SEASONAL }

interface PricingStrategy {
    PricingType type();
    Money calculate(Order order);
}

@Component
class PricingStrategies {
    private final Map<PricingType, PricingStrategy> byType;

    PricingStrategies(List<PricingStrategy> all) {                   // Spring injects every implementation
        this.byType = all.stream().collect(Collectors.toMap(PricingStrategy::type, s -> s));
    }

    PricingStrategy forType(PricingType type) {
        PricingStrategy s = byType.get(type);
        if (s == null) throw new IllegalArgumentException("No pricing strategy for " + type);
        return s;
    }
}

Adding a new pricing rule is now one new class annotated with @Component. Neither the registry nor any caller changes. Failing loudly on an unknown type is deliberate: a silent fallback to "regular price" would hide configuration bugs.

2. Selection by rule, when the choice depends on the data rather than an explicit type:

class PricingPolicy {
    private final List<Map.Entry<Predicate<Order>, PricingStrategy>> rules;   // first match wins
    private final PricingStrategy fallback;

    PricingPolicy(List<Map.Entry<Predicate<Order>, PricingStrategy>> rules, PricingStrategy fallback) {
        this.rules = rules; this.fallback = fallback;
    }

    PricingStrategy choose(Order order) {
        return rules.stream().filter(r -> r.getKey().test(order)).map(Map.Entry::getValue)
                    .findFirst().orElse(fallback);
    }
}

The ordering of rules is now data, visible in one place, and testable, rather than spread across nested conditionals.

Strategies with lambdas: when a class is overkill

For single-method strategies with no state, Java's functional interfaces are enough:

Map<PricingType, Function<Order, Money>> pricing = Map.of(
    PricingType.REGULAR,  Order::subtotal,
    PricingType.BULK,     o -> o.subtotal().times(0.90),
    PricingType.SEASONAL, o -> o.subtotal().times(0.85));

List<Employee> sorted = employees.stream()
        .sorted(Comparator.comparing(Employee::department).thenComparing(Employee::salary).reversed())
        .toList();                                       // Comparator IS a strategy object

Comparator, Runnable, Function and friends are the JDK's everyday strategies. Promote a lambda to a named class when it needs dependencies (a tax-rate service), configuration, multiple methods, or its own tests.

Testing strategies

Each strategy is tested alone, with simple inputs, and the context class is tested with a stub strategy. Neither test needs the other's complexity:

@Test void bulkPricingGivesTenPercentOff() {
    Order order = orderWithSubtotal(Money.of(1000));
    assertEquals(Money.of(900), new BulkDiscountPricing().calculate(order));
}

@Test void checkoutUsesWhateverStrategyItIsGiven() {
    Checkout checkout = new Checkout(order -> Money.of(42));     // a stub strategy via a lambda
    assertEquals(Money.of(42), checkout.total(anyOrder()));
}

This separation is one of the pattern's most practical benefits. Pricing edge cases (rounding, minimum order values) are tested once, in the strategy, not re-tested through every class that uses pricing.

When not to use Strategy

  • There's only one algorithm, and no realistic second one. The interface is speculative abstraction.
  • The variations are tiny and stable (two branches that haven't changed in years). A simple conditional is clearer.
  • The "strategies" need deep access to the context's internals. That's a sign the responsibilities are split in the wrong place.

Follow-up questions this topic invites — and their answers

Q: How is Strategy different from just passing a lambda/functional interface? A: For simple, single-method behavior, a Function/Comparator-style lambda often is a lightweight Strategy — Java's functional interfaces are Strategy without the ceremony of a named class. Strategy as a named-class pattern earns its keep when the "algorithm" needs multiple methods, internal state, or a constructor with configuration — beyond what a bare lambda can express.

Q: Isn't Strategy the same pattern as Bridge? A: Structurally similar (both compose a reference instead of inheriting), but different intent and scope: Strategy typically swaps one algorithm at a single decision point; Bridge decouples two entire class hierarchies so both can evolve independently over an object's whole lifetime (see Bridge & Flyweight).

Q: Where would you put the logic that decides WHICH strategy to use? A: That decision itself is exactly what a Factory (see Factory & Abstract Factory) is for — Strategy and Factory commonly pair up: a factory method takes a type/condition and returns the appropriate PricingStrategy instance, keeping the selection logic in one place too.

Q: How do you choose a strategy at runtime without reintroducing a big switch statement? A: Build a map from a key (an enum, or a string from configuration) to strategy instances. In Spring, inject List<PricingStrategy> and index it by each strategy's declared type. For data-dependent choices, use an ordered list of predicate → strategy rules.

Q: Can a strategy hold state? A: Yes, but prefer immutable configuration (rates, thresholds) set at construction. Mutable per-call state inside a strategy that's shared between threads (for example, a Spring singleton) is a concurrency bug. Keep per-call data in method parameters and local variables.

Q: How does Strategy relate to dependency injection? A: DI is the mechanism that supplies the chosen strategy to the class that uses it. Strategy is the design that makes the behaviour swappable in the first place. Together, they let configuration or profiles decide behaviour without code changes.

Advertisement

Previous

Bridge & Flyweight

Next

Observer Pattern

AI Tutor

Lesson: Strategy Pattern

Quick actions

AI responses can be inaccurate. Verify critical information.