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
- SOLID Principles β Strategy embodies OCP
- Inheritance vs Composition β Strategy uses composition
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:
- Parking allocation (NearestFirst, SpreadEvenly, FloorWise)
- Payment processing (CreditCard, UPI, Wallet)
- Sorting preferences (ByPrice, ByRating, ByDistance)
- Compression algorithms (Gzip, LZ4, Snappy)
- Authentication methods (OAuth, JWT, APIKey)
Avoid Strategy when:
- You have exactly 2 options and both are 3-line lambdas
- The βalgorithmβ is just a configuration value, not logic
- Thereβs no runtime switching β a simple template method or inheritance works
- Youβre adding patterns preemptively with no current need (YAGNI)
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
-
β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.
-
β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.
-
β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.
-
β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.
-
β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 |