Limited time: AI code review, hints, mock interviews, whiteboard analysis, and all Pro features are unlocked. Enroll
⏱️ 9 min read

Strategy Pattern

The Strategy pattern defines a family of algorithms, encapsulates each one in its own class, and makes them interchangeable at runtime. The client picks which strategy to use without knowing the algorithm’s internals.

Why this matters: Strategy is the single most useful pattern in machine coding rounds. Whenever you see β€œthe system should support multiple types of X” (pricing, sorting, routing, allocation), this is your tool. It directly satisfies the Open-Closed Principle – new algorithms require zero changes to existing code.


Prerequisites


Class Diagram

classDiagram
    class Context {
        -strategy: Strategy
        +setStrategy(Strategy)
        +executeAction()
    }
    class Strategy {
        <<interface>>
        +execute(data)
    }
    class ConcreteStrategyA {
        +execute(data)
    }
    class ConcreteStrategyB {
        +execute(data)
    }
    class ConcreteStrategyC {
        +execute(data)
    }

    Context --> Strategy : uses
    Strategy <|.. ConcreteStrategyA
    Strategy <|.. ConcreteStrategyB
    Strategy <|.. ConcreteStrategyC

The Problem: Growing If-Else Chains

Every time someone adds a new algorithm variant, the existing class gets another branch. This violates OCP, makes testing harder, and creates merge conflicts when multiple developers add variants simultaneously.

// Before: every new rate-limiting algorithm modifies this class
public class RateLimiter {
    public boolean allowRequest(String clientId, String algorithm) {
        if (algorithm.equals("TOKEN_BUCKET")) {
            // 30 lines of token bucket logic
        } else if (algorithm.equals("SLIDING_WINDOW")) {
            // 30 lines of sliding window logic  
        } else if (algorithm.equals("FIXED_WINDOW")) {
            // 30 lines of fixed window logic
        } else if (algorithm.equals("LEAKY_BUCKET")) {
            // someone just added this last sprint...
        }
        return false;
    }
}
# Before: the function keeps growing
def allow_request(client_id: str, algorithm: str) -> bool:
    if algorithm == "TOKEN_BUCKET":
        # 30 lines...
    elif algorithm == "SLIDING_WINDOW":
        # 30 lines...
    elif algorithm == "FIXED_WINDOW":
        # 30 lines...
    elif algorithm == "LEAKY_BUCKET":
        # added last week...
    return False
// Before: switch statement that nobody wants to touch
bool RateLimiter::allowRequest(const string& clientId, const string& algo) {
    if (algo == "TOKEN_BUCKET") {
        // 30 lines
    } else if (algo == "SLIDING_WINDOW") {
        // 30 lines
    } else if (algo == "FIXED_WINDOW") {
        // 30 lines
    }
    return false;
}

The Solution: Strategy Pattern

// Strategy interface
public interface RateLimitAlgorithm {
    boolean allowRequest(String clientId);
    void recordRequest(String clientId);
}

// Concrete strategy: Token Bucket
public class TokenBucketLimiter implements RateLimitAlgorithm {
    private final int maxTokens;
    private final Duration refillInterval;
    private final Map<String, TokenBucket> buckets = new ConcurrentHashMap<>();

    public TokenBucketLimiter(int maxTokens, Duration refillInterval) {
        this.maxTokens = maxTokens;
        this.refillInterval = refillInterval;
    }

    @Override
    public boolean allowRequest(String clientId) {
        TokenBucket bucket = buckets.computeIfAbsent(clientId,
            k -> new TokenBucket(maxTokens, refillInterval));
        return bucket.tryConsume();
    }

    @Override
    public void recordRequest(String clientId) {
        // Token already consumed in allowRequest
    }
}

// Concrete strategy: Sliding Window Counter
public class SlidingWindowLimiter implements RateLimitAlgorithm {
    private final int maxRequests;
    private final Duration windowSize;
    private final Map<String, Deque<Instant>> windows = new ConcurrentHashMap<>();

    public SlidingWindowLimiter(int maxRequests, Duration windowSize) {
        this.maxRequests = maxRequests;
        this.windowSize = windowSize;
    }

    @Override
    public boolean allowRequest(String clientId) {
        Deque<Instant> timestamps = windows.computeIfAbsent(clientId,
            k -> new ConcurrentLinkedDeque<>());
        Instant cutoff = Instant.now().minus(windowSize);
        while (!timestamps.isEmpty() && timestamps.peekFirst().isBefore(cutoff)) {
            timestamps.pollFirst();
        }
        return timestamps.size() < maxRequests;
    }

    @Override
    public void recordRequest(String clientId) {
        windows.get(clientId).addLast(Instant.now());
    }
}

// Context: RateLimiter doesn't know which algorithm is active
public class RateLimiter {
    private RateLimitAlgorithm algorithm;

    public RateLimiter(RateLimitAlgorithm algorithm) {
        this.algorithm = algorithm;
    }

    public void setAlgorithm(RateLimitAlgorithm algorithm) {
        this.algorithm = algorithm;
    }

    public boolean handleRequest(String clientId) {
        if (!algorithm.allowRequest(clientId)) {
            return false; // 429 Too Many Requests
        }
        algorithm.recordRequest(clientId);
        return true;
    }
}
from abc import ABC, abstractmethod
from collections import deque
from time import time

class RateLimitAlgorithm(ABC):
    @abstractmethod
    def allow_request(self, client_id: str) -> bool: ...

    @abstractmethod
    def record_request(self, client_id: str) -> None: ...

class TokenBucketLimiter(RateLimitAlgorithm):
    def __init__(self, max_tokens: int, refill_rate: float):
        self._max_tokens = max_tokens
        self._refill_rate = refill_rate  # tokens per second
        self._buckets: dict[str, dict] = {}

    def _get_bucket(self, client_id: str) -> dict:
        if client_id not in self._buckets:
            self._buckets[client_id] = {
                "tokens": self._max_tokens,
                "last_refill": time()
            }
        bucket = self._buckets[client_id]
        # Refill tokens based on elapsed time
        elapsed = time() - bucket["last_refill"]
        bucket["tokens"] = min(
            self._max_tokens,
            bucket["tokens"] + elapsed * self._refill_rate
        )
        bucket["last_refill"] = time()
        return bucket

    def allow_request(self, client_id: str) -> bool:
        bucket = self._get_bucket(client_id)
        return bucket["tokens"] >= 1

    def record_request(self, client_id: str) -> None:
        self._buckets[client_id]["tokens"] -= 1

class SlidingWindowLimiter(RateLimitAlgorithm):
    def __init__(self, max_requests: int, window_seconds: float):
        self._max_requests = max_requests
        self._window = window_seconds
        self._timestamps: dict[str, deque] = {}

    def allow_request(self, client_id: str) -> bool:
        if client_id not in self._timestamps:
            self._timestamps[client_id] = deque()
        dq = self._timestamps[client_id]
        cutoff = time() - self._window
        while dq and dq[0] < cutoff:
            dq.popleft()
        return len(dq) < self._max_requests

    def record_request(self, client_id: str) -> None:
        self._timestamps[client_id].append(time())

# Context
class RateLimiter:
    def __init__(self, algorithm: RateLimitAlgorithm):
        self._algorithm = algorithm

    def set_algorithm(self, algorithm: RateLimitAlgorithm):
        self._algorithm = algorithm

    def handle_request(self, client_id: str) -> bool:
        if not self._algorithm.allow_request(client_id):
            return False
        self._algorithm.record_request(client_id)
        return True
#include <string>
#include <unordered_map>
#include <deque>
#include <chrono>
#include <memory>

using namespace std;
using Clock = chrono::steady_clock;
using TimePoint = chrono::time_point<Clock>;

class RateLimitAlgorithm {
public:
    virtual ~RateLimitAlgorithm() = default;
    virtual bool allowRequest(const string& clientId) = 0;
    virtual void recordRequest(const string& clientId) = 0;
};

class SlidingWindowLimiter : public RateLimitAlgorithm {
    int maxRequests_;
    chrono::seconds windowSize_;
    unordered_map<string, deque<TimePoint>> windows_;
public:
    SlidingWindowLimiter(int maxReqs, chrono::seconds window)
        : maxRequests_(maxReqs), windowSize_(window) {}

    bool allowRequest(const string& clientId) override {
        auto now = Clock::now();
        auto& dq = windows_[clientId];
        auto cutoff = now - windowSize_;
        while (!dq.empty() && dq.front() < cutoff) {
            dq.pop_front();
        }
        return static_cast<int>(dq.size()) < maxRequests_;
    }

    void recordRequest(const string& clientId) override {
        windows_[clientId].push_back(Clock::now());
    }
};

// Context
class RateLimiter {
    unique_ptr<RateLimitAlgorithm> algorithm_;
public:
    explicit RateLimiter(unique_ptr<RateLimitAlgorithm> algo)
        : algorithm_(std::move(algo)) {}

    void setAlgorithm(unique_ptr<RateLimitAlgorithm> algo) {
        algorithm_ = std::move(algo);
    }

    bool handleRequest(const string& clientId) {
        if (!algorithm_->allowRequest(clientId)) return false;
        algorithm_->recordRequest(clientId);
        return true;
    }
};

Runtime Swapping

Strategy’s key advantage: switch algorithms without restarting or rebuilding.

// Adaptive: switch algorithm based on traffic pattern
public class AdaptiveRateLimiter extends RateLimiter {
    private final RateLimitAlgorithm burstAlgo;
    private final RateLimitAlgorithm steadyAlgo;

    public AdaptiveRateLimiter() {
        super(new TokenBucketLimiter(100, Duration.ofSeconds(1)));
        this.burstAlgo = new TokenBucketLimiter(100, Duration.ofSeconds(1));
        this.steadyAlgo = new SlidingWindowLimiter(1000, Duration.ofMinutes(1));
    }

    public void onTrafficSpike() {
        setAlgorithm(burstAlgo);  // tighter per-second control
    }

    public void onNormalTraffic() {
        setAlgorithm(steadyAlgo); // relaxed per-minute window
    }
}
# Config-driven strategy selection
def create_limiter(config: dict) -> RateLimiter:
    algo_type = config["algorithm"]
    if algo_type == "token_bucket":
        algo = TokenBucketLimiter(config["max_tokens"], config["refill_rate"])
    elif algo_type == "sliding_window":
        algo = SlidingWindowLimiter(config["max_requests"], config["window_seconds"])
    else:
        raise ValueError(f"Unknown algorithm: {algo_type}")
    return RateLimiter(algo)

# Swap at runtime via config reload
limiter = create_limiter(load_config())
# ... later, config changes ...
limiter.set_algorithm(TokenBucketLimiter(50, 0.5))
// Factory + runtime swap
unique_ptr<RateLimitAlgorithm> createFromConfig(const Config& cfg) {
    if (cfg.algorithm == "sliding_window") {
        return make_unique<SlidingWindowLimiter>(
            cfg.maxRequests, chrono::seconds(cfg.windowSecs));
    }
    // default: token bucket
    return make_unique<TokenBucketLimiter>(cfg.maxTokens, cfg.refillRate);
}

// Swap on config reload
limiter.setAlgorithm(createFromConfig(newConfig));

Strategy vs If-Else: When Each is Appropriate

Use Strategy When Keep If-Else When
3+ algorithm variants exist or are expected Only 2 simple branches that won’t grow
Each algorithm is complex (10+ lines) Logic is a trivial one-liner per branch
New variants will be added in the future The option set is permanently fixed
You need to test each algorithm in isolation Testing the containing method is sufficient
Runtime selection is needed Branch is determined at compile time
Algorithms need their own state All branches use the same local variables

When to Use vs When to Avoid

Use Strategy for:

Avoid Strategy when:


Strategy vs State: The Common Confusion

Dimension Strategy State
Who decides Client explicitly sets the algorithm Object transitions internally
Awareness Strategies don’t know about each other States know their valid transitions
Statefulness Usually stateless Always maintain context state
Lifecycle Set once or swapped externally Continuously evolving
Example User picks β€œsort by price” Order moves from PLACED to SHIPPED

Interview Questions

  1. β€œHow is Strategy different from State?” – Strategy is externally selected by the client; State transitions happen internally. Strategies are typically stateless; States carry and modify context.

  2. β€œCan you use lambdas instead of full Strategy classes?” – Yes, for single-method strategies in Java 8+ or Python. But once the algorithm needs its own state or multiple methods, a class is cleaner.

  3. β€œWhy not just use inheritance?” – Inheritance locks in behavior at compile time. Strategy uses composition, enabling runtime flexibility. You also avoid the fragile base class problem.

  4. β€œWhat if I only have two strategies?” – If both are simple and unlikely to grow, if-else is fine. Strategy adds value when you expect growth or need runtime swapping.

  5. β€œHow do you decide which strategy to use at runtime?” – Either the caller decides (user preference), or a factory selects based on context (traffic level, config, A/B test flags).


See It in Action

Parking Lot Music Player Rate Limiter URL Shortener

Free system design + DSA prep. If it helped you crack an interview, consider supporting.

SensAI SensAI
Beta
Listening...
Tap mic to stop voice mode

Shape what we build next

Every piece of feedback is read by the team and directly influences our roadmap.

What type of feedback?

Install SystemCraft

Add to your home screen for instant access, offline reading, and a distraction-free experience.

Offline reading Faster loads No browser tabs App-like feel

Unlock AI Features

One click to activate - no payment, no credit card. Just sign in and you're in.

AI code review and hints
SensAI chat assistant
AI mock interviews
Whiteboard analysis
100% free during early access