- Chapter
- Chapter 1 — Strategy
- Prerequisites
- Volume II (OCP, DIP); Volume III Chapter 11 Topic 11.4 (Bridge vs Strategy)
- You will be able to
- Explain the Strategy structure, encapsulate a family of algorithms behind one interface, select strategies at runtime from configuration or data, and keep contexts free of algorithm-specific branching
- Covers
- Encapsulated algorithms; Runtime selection
On this page
- 1. Concept / Definition
- 2. Why It Exists
- 3. Core Concepts
- 4. How It Works Internally — Delegation and Dispatch
- 4A. Deep Dive — Strategies With Data, Composition and Validation
- 5. APIs / Features / Syntax
- 6. Practical Examples
- 7. Common Problems & Mistakes
- 8. Performance & Production Considerations
- 9. Architecture & Design Considerations
- 10. Interview Questions & Answers
1Concept / Definition
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. The object that uses an algorithm — the context — depends only on the strategy interface, so the algorithm can vary independently of the clients that use it.

| Role | Responsibility | Example |
|---|---|---|
| Strategy | Interface declaring the algorithm | ShippingPolicy.cost(parcel, dest) |
| Concrete strategy | One implementation | FlatRate, ByWeight, FreeOver50 |
| Context | Uses a strategy as part of its own work | Checkout.total(cart) |
| Client / selector | Chooses which strategy the context gets | Registry keyed by customer segment |
2Why It Exists
Business rules vary: shipping costs by country, discounts by segment, retries by failure type, sorting by user preference. Embedding every variant inside the using class produces long conditionals that grow with each new rule, mix unrelated concerns and require touching (and retesting) the same class for every change. Strategy isolates each variant, lets new variants be added without modifying existing code (Open/Closed Principle) and allows the choice to be made at runtime — per request, per tenant or per configuration.
3Core Concepts
3.1 Encapsulated algorithms
Encapsulation means each algorithm lives in its own unit with a clear contract, hidden from the context.
public interface ShippingPolicy { // Strategy
Money cost(Parcel parcel, Destination destination);
}
public record FlatRate(Money amount) implements ShippingPolicy { // Concrete strategies
public Money cost(Parcel p, Destination d) { return amount; }
}
public record ByWeight(Money base, Money perKg) implements ShippingPolicy {
public Money cost(Parcel p, Destination d) {
return base.plus(perKg.times(p.weightKg()));
}
}
public record FreeOver(Money threshold, ShippingPolicy otherwise)
implements ShippingPolicy {
public Money cost(Parcel p, Destination d) {
return p.value().isAtLeast(threshold) ? Money.ZERO : otherwise.cost(p, d);
}
}The context never inspects which strategy it holds:
public final class Checkout { // Context
private final ShippingPolicy shipping;
private final TaxCalculator tax;
public Checkout(ShippingPolicy shipping, TaxCalculator tax) {
this.shipping = shipping;
this.tax = tax;
}
public Money total(Cart cart, Destination dest) {
Money subtotal = cart.subtotal();
Money ship = shipping.cost(cart.parcel(), dest); // delegate the variation
return subtotal.plus(ship).plus(tax.on(subtotal.plus(ship), dest));
}
}| Property of a well-encapsulated strategy | Why it matters |
|---|---|
| One responsibility (one decision) | Easy to name, test and replace |
| Depends only on its inputs and injected collaborators | No hidden coupling to the context |
| Same contract for all implementations (LSP) | Context works with any of them |
| Stateless or immutable parameters | Safe to share across threads |
Note that FreeOver composes another strategy — strategies can be combined like decorators when their contract allows it.
3.2 Runtime selection
Strategies become powerful when the choice is made from data at runtime.

@Component
public final class ShippingPolicies {
private final Map<Segment, ShippingPolicy> bySegment;
private final ShippingPolicy standard;
public ShippingPolicies(ShippingProps props) {
this.standard = new ByWeight(props.base(), props.perKg());
this.bySegment = Map.of(
Segment.GOLD, new FreeOver(Money.eur("50"), standard),
Segment.STAFF, new FlatRate(Money.ZERO),
Segment.B2B, new FlatRate(props.b2bFlat()));
}
public ShippingPolicy forCustomer(Customer c) {
return bySegment.getOrDefault(c.segment(), standard); // one selection point
}
}
| Selection source | Example | Typical binding |
|---|---|---|
| Configuration | pricing.strategy=tiered | At startup |
| Tenant settings | Brand A uses weight-based shipping | Per tenant, cached |
| Request data | Customer segment, country, device | Per request |
| Feature flag | Canary new algorithm for 5% of traffic | Per request |
| Runtime metrics | Switch load-balancing algorithm under stress | Dynamic |
Keeping selection in one component (registry, factory, Spring bean map) means adding a strategy changes only that component and a new class — never the context.
4How It Works Internally — Delegation and Dispatch
1. The selector returns a strategy instance typed as the interface.
2. The context stores it (constructor injection) or receives it per call (method parameter).
3. When the context reaches the varying step, it calls the interface method.
4. The JVM dispatches the call to the concrete implementation — a virtual call, typically inlined by the JIT when one or two implementations dominate at a call site.
5. The result flows back; the context continues its algorithm.
Two binding styles exist:
| Style | Code shape | Use when |
|---|---|---|
| Strategy per context instance | new Checkout(policy) | Strategy fixed for the context's lifetime |
| Strategy per call | checkout.total(cart, policy) | Strategy varies per request and context is shared |
4ADeep Dive — Strategies With Data, Composition and Validation
Real strategies often carry parameters (rates, thresholds). Records make them value objects that can be loaded from configuration, compared and logged:
public sealed interface DiscountPolicy permits NoDiscount, Percent, Tiered {
Money discount(Cart cart);
}
public record NoDiscount() implements DiscountPolicy {
public Money discount(Cart c) { return Money.ZERO; }
}
public record Percent(BigDecimal rate) implements DiscountPolicy {
public Percent {
if (rate.signum() < 0 || rate.compareTo(new BigDecimal("0.5")) > 0)
throw new IllegalArgumentException("rate out of range: " + rate);
}
public Money discount(Cart c) { return c.subtotal().times(rate); }
}
public record Tiered(List<Tier> tiers) implements DiscountPolicy {
public Money discount(Cart c) {
return tiers.stream().filter(t -> c.subtotal().isAtLeast(t.from()))
.reduce((a, b) -> b).map(t -> c.subtotal().times(t.rate()))
.orElse(Money.ZERO);
}
}| Technique | Benefit |
|---|---|
| Records as strategies | Parameters validated in the compact constructor; toString for logs |
| Sealed strategy interface | Closed, reviewable set; exhaustive handling in admin UIs and serialisation |
Composing strategies (FreeOver(threshold, otherwise)) | New behaviour without new classes |
| Default/fallback strategy | Selection never returns null |
Sealing the interface is a deliberate choice: it prevents external extension (good for business rules owned by one team), whereas an open interface invites plug-ins (good for platform extension points).
5APIs / Features / Syntax
| JDK / Spring API | Strategy it represents |
|---|---|
Comparator<T> | Ordering algorithm for sorting |
java.util.function.* | Generic algorithm shapes (Topic 1.2) |
ThreadPoolExecutor + RejectedExecutionHandler | Policy when the queue is full |
javax.crypto.Cipher transformations | Cipher algorithm selection |
Spring Security PasswordEncoder | Hashing algorithm |
Spring TaskScheduler Trigger | Scheduling algorithm (cron, periodic) |
Resilience4j IntervalFunction | Back-off algorithm |

var executor = new ThreadPoolExecutor(8, 16, 60, SECONDS,
new ArrayBlockingQueue<>(1_000),
new ThreadPoolExecutor.CallerRunsPolicy()); // strategy for overload6Practical Examples
6.1 Retry back-off strategies
@FunctionalInterface
public interface BackOff { Duration delay(int attempt); }
BackOff fixed = a -> Duration.ofMillis(200);
BackOff exponential = a -> Duration.ofMillis(100L << Math.min(a, 10));
BackOff jittered = a -> exponential.delay(a)
.plusMillis(ThreadLocalRandom.current().nextLong(100));6.2 Testing a context with a stub strategy
@Test
void totalAddsShippingFromPolicy() {
ShippingPolicy fixed = (p, d) -> Money.eur("7.00");
var checkout = new Checkout(fixed, TaxCalculator.NONE);
assertThat(checkout.total(cartOf("40.00"), anyDest())).isEqualTo(Money.eur("47.00"));
}6.3 Canary-releasing a new algorithm
ShippingPolicy select(Customer c) {
return flags.isEnabled("new-shipping", c.id())
? newZoneBasedPolicy
: policies.forCustomer(c);
}The new algorithm runs for a percentage of customers; metrics tagged by policy name compare results before full rollout.
7Common Problems & Mistakes
- Context still branching on the strategy type (
if (policy instanceof FlatRate)). - Strategies reaching into the context instead of receiving inputs.
- Selection logic scattered across services instead of one registry.
- Stateful strategies shared across threads.
- null when no strategy matches — always provide a default.
- Strategy with many methods used by many clients — likely a Bridge implementor.
8Performance & Production Considerations
| Aspect | Guidance |
|---|---|
| Call overhead | One interface call; negligible, often inlined |
| Megamorphic call sites | Many implementations at one hot site reduce inlining — rarely significant |
| Observability | Tag metrics and logs with the strategy name to compare behaviour |
| Configuration errors | Validate strategy parameters at startup (fail fast) |
| Rollout | Feature-flag new strategies; keep old ones for rollback |
9Architecture & Design Considerations
- Strategies are the natural shape for business policies owned by domain teams.
- Treat the strategy registry as a single, testable component; validate completeness (every segment has a policy).
- Decide between closed (sealed) and open (plug-in) strategy families deliberately.
- At system scale, strategies become configurable rules per tenant — version them and audit changes.
10Interview Questions & Answers
What is the Strategy pattern?
Strategy is a behavioural pattern that defines a family of interchangeable algorithms, puts each behind a common interface, and lets the code that uses them choose one at run time without depending on its details. For example, a checkout service might calculate shipping costs with a StandardShipping, ExpressShipping or FreeShipping strategy, all implementing a ShippingCostStrategy interface. The context calls the interface and does not care which strategy it holds. New algorithms can be added without changing existing code, and each one can be tested on its own. In modern Java, simple strategies are often just lambdas.
What are the roles in Strategy?
Strategy has three main roles. The strategy interface declares the operation that all algorithms provide, such as calculate(order). Concrete strategies implement that interface, each with a different algorithm, such as standard, express or free shipping. The context is the class that uses a strategy to do part of its work, such as the checkout service, and holds a reference to the strategy interface rather than any concrete class. In practice there is usually a fourth part: a client, selector or registry that decides which strategy the context should use, for example based on the customer's delivery option or a configuration value.
Where should strategy selection happen?
Selection should happen in one clearly owned place, such as a registry, a factory or a configuration component. A common Spring approach is to inject all strategy beans into a map keyed by name, and have a selector choose by delivery option, tenant or feature flag. Keeping selection in one place means the rules for choosing are easy to find, test and change, and the context stays free of conditional logic. If selection logic is scattered, with if statements in the context and its callers, adding a strategy means hunting through the code and the pattern loses most of its benefit.
Answer: The JDK and Spring use Strategy widely. java.util.Comparator is a strategy for ordering, passed to sort methods that do not care how elements are compared. RejectedExecutionHandler is a strategy that a ThreadPoolExecutor uses when its queue is full, with implementations such as AbortPolicy and CallerRunsPolicy. In Spring Security, PasswordEncoder is a strategy for hashing and checking passwords, with BCrypt, Argon2 and delegating implementations. Other examples include Spring's TaskExecutor, retry back-off policies and Jackson's PropertyNamingStrategy. Recognising them helps you design your own strategies in the same style.How do records and sealed interfaces improve strategies?
Records make good strategy parameters and simple strategies. A strategy's configuration, such as a discount percentage and a cap, can be a record that validates itself in its compact constructor, so an invalid strategy cannot be created. Sealed interfaces close the family of strategies: the interface lists exactly which implementations are permitted, so the compiler can check that a switch over them is exhaustive, and no other team can quietly add an implementation. Together they turn a strategy family into a precise, compiler-checked model, which is well suited to business policies owned by one team, such as pricing or eligibility rules.
How do you roll out a new algorithm safely?
Treat the new algorithm as a new strategy rather than changing the old one in place. Implement and test it alongside the existing strategy, then select it through a feature flag for a small share of traffic, for example 5% of customers or one region. Tag metrics and logs with the strategy name, so you can compare business and technical results, such as conversion rate, error rate and latency, between old and new. Increase the share gradually if the results are good. Keep the old strategy available, so rollback is a configuration change rather than a deployment, and remove it once the new one is fully proven.
When should a strategy family be open vs sealed?
Make a strategy family open when other teams need to extend it without changing your code, such as a platform extension point for payment providers, notification channels or export formats. An open family needs a stable, documented interface, versioning and validation of third-party implementations. Make it sealed when the family represents business policies owned and reviewed by one team, such as pricing rules or risk scores. Sealing means every variant is known, the compiler checks exhaustive handling, and new variants require a deliberate change by the owning team. Choose deliberately, because changing a widely used open interface later is expensive.
How would you design tenant-specific pricing policies across a SaaS platform?
Model each pricing policy as a strategy implementing a common interface, with its parameters held in validated records, so tenants choose policies and settings rather than writing code. Store each tenant's policy configuration in a versioned, audited store, so changes are traceable and can be rolled back. At load time, build a registry that maps tenants to configured strategies, falling back to a default policy when a tenant has none. Validate configurations before they go live, and expose metrics per policy and tenant, such as average price and error rates. For truly custom needs, offer a controlled extension mechanism rather than ad hoc code changes.