Java design pattern deep dive
Strategy Pattern in Java: 100 interview questions with professional answers.
Learn how the Strategy pattern encapsulates a family of interchangeable algorithms behind one interface, why Comparator and lambdas made it the JDK's most common behavioral pattern, how Spring wires strategies as beans, and how to tell Strategy apart from State, Template Method, Bridge, and Command in an interview.
What makes a good Strategy answer?
Interviewers want to see that you understand delegation and swappability, not just "an interface with several classes": correct dependency direction, honest statelessness, and restraint about when the full pattern is even warranted.
| Approach | Use when | Watch out for |
|---|---|---|
| Classic interface + ConcreteStrategy classes | Each algorithm needs multiple methods, internal state, or its own constructor-injected dependencies. | Boilerplate for genuinely simple, stateless behavior that could be a one-line lambda. |
| Functional interface + lambda strategy | The strategy is a single stateless operation, such as a comparison, mapping, or predicate. | Cannot hold meaningful per-instance state or implement more than one abstract method. |
| Enum-based strategy (constant-specific bodies) | The set of strategies is small, fixed, and known entirely at compile time. | Cannot be extended by third parties or configuration without editing the enum's source. |
Spring multiple beans + @Qualifier/map | Strategies need dependency injection, per-environment activation, or centralized lookup by name. | Ambiguous bean resolution if a qualifier or map-based lookup is not wired correctly. |
Topics
Interview questions and answers
Each answer gives the implementation direction, the trade-off to mention, and the production concern that makes the answer stronger.
1. Explain the Strategy design pattern in Java: what problem does it solve, and what are the three participants (Strategy interface, ConcreteStrategy, Context) in the classic GoF structure?
Strategy defines a family of interchangeable algorithms behind one common interface, letting the algorithm vary independently from the code that uses it. It solves the problem of a class needing several variants of a behavior (how to pay, how to sort, how to price a shipment) without hardcoding every variant as a branch inside that class.
The Strategy interface declares the algorithm's contract; each ConcreteStrategy implements one variant of that algorithm; the Context holds a reference to a Strategy and delegates the actual work to it, remaining completely ignorant of which concrete implementation is plugged in.
interface PaymentStrategy {
void pay(java.math.BigDecimal amount);
}
class CreditCardStrategy implements PaymentStrategy {
public void pay(java.math.BigDecimal amount) { /* charge card */ }
}
class CheckoutContext {
private final PaymentStrategy strategy;
CheckoutContext(PaymentStrategy strategy) { this.strategy = strategy; }
void checkout(java.math.BigDecimal total) { strategy.pay(total); }
}
2. What must a well-designed Strategy interface expose, and why should it model a single cohesive algorithm contract rather than a bag of unrelated methods?
A good Strategy interface exposes exactly the operation the Context needs to delegate, expressed in terms of the Context's domain (amounts, requests, records) rather than in terms of any one implementation's internals. Ideally it declares a single abstract method so it doubles as a target for lambdas and method references.
Bundling unrelated methods onto the interface forces every ConcreteStrategy to implement operations it may not need, and it prevents the interface from being usable as a @FunctionalInterface, pushing implementers back toward heavier class-based strategies even for trivial behavior.
3. What are a ConcreteStrategy implementation's responsibilities, and why should it avoid depending on the Context that will use it?
A ConcreteStrategy's only responsibility is to fully and correctly implement the Strategy interface's contract for one specific algorithm variant. It should not hold a back-reference to its Context, request Context-specific data outside the method parameters it was given, or assume it is the only strategy instance in the system.
Avoiding a Context dependency keeps the strategy reusable across any Context that needs that algorithm, testable entirely in isolation, and safe to share as a single instance if it happens to be stateless.
4. Describe the Context class's role in the Strategy pattern: what does it own, what does it delegate, and why should it never contain conditional logic that picks behavior by type?
The Context owns the workflow around the algorithm (input validation, orchestration, what to do with the result) and holds a reference to whichever Strategy was supplied to it. It delegates the one varying step, the algorithm itself, entirely to that reference.
If the Context contains an if (type == X) chain choosing behavior internally, it has silently re-implemented the very conditional logic Strategy exists to eliminate, and adding a new variant now requires editing the Context instead of simply supplying a new implementation (see Q71).
5. Compare constructor injection versus setter injection for supplying a Strategy to a Context, and explain which is generally preferred in production code and why.
Constructor injection makes the Strategy a required, immutable dependency: the Context cannot exist in a half-configured state, and the compiler and tests both make the dependency obvious. Setter injection allows the strategy to be swapped after construction, useful for long-lived Context objects whose behavior must change at runtime, but it opens a window where the field can be null or read concurrently mid-swap.
class ShippingCalculator {
private final ShippingCostStrategy strategy; // constructor-injected, immutable
ShippingCalculator(ShippingCostStrategy strategy) {
this.strategy = java.util.Objects.requireNonNull(strategy);
}
}
Constructor injection is the default; reach for setter injection only when runtime reconfiguration is an explicit requirement, and even then prefer replacing the whole Context or using an atomic reference over a plain mutable field.
6. When would you deliberately allow a Context's Strategy to be swapped after construction via a setter method, and what precautions must the setter take to remain thread-safe?
A setter makes sense for a long-lived, shared Context whose behavior should change without recreating it, for example a rendering engine whose compression strategy is reconfigured from an admin panel while requests keep flowing. The setter must publish the new reference safely across threads.
class CompressionContext {
private volatile CompressionStrategy strategy;
void setStrategy(CompressionStrategy strategy) {
this.strategy = java.util.Objects.requireNonNull(strategy);
}
byte[] compress(byte[] data) { return strategy.compress(data); } // reads current volatile value
}
volatile or an AtomicReference for any strategy field mutated after construction.7. Design a factory that selects and returns the correct ConcreteStrategy implementation based on a configuration key or request attribute, and explain why the Context should depend only on the factory's return type, not the factory itself.
The factory centralizes the mapping from a key (a payment method name, a tier code) to a concrete implementation, so that logic exists in exactly one place instead of being copy-pasted at every call site.
final class PaymentStrategyFactory {
static PaymentStrategy create(String method) {
return switch (method) {
case "CREDIT_CARD" -> new CreditCardStrategy();
case "PAYPAL" -> new PayPalStrategy();
case "WALLET" -> new WalletStrategy();
default -> throw new IllegalArgumentException("Unknown payment method: " + method);
};
}
}
The Context should only ever hold a PaymentStrategy reference obtained from the factory at the call site that assembles the Context; it should never import or call the factory itself, keeping the Context reusable in tests with any strategy, factory-produced or hand-built.
8. Implement a Strategy registry, a Map keyed by name, that lets new strategies be registered and looked up at runtime without modifying existing code — how does this support the open/closed principle?
class StrategyRegistry<T> {
private final java.util.Map<String, T> strategies = new java.util.concurrent.ConcurrentHashMap<>();
void register(String key, T strategy) { strategies.put(key, strategy); }
T resolve(String key) {
T strategy = strategies.get(key);
if (strategy == null) {
throw new IllegalArgumentException("No strategy registered for: " + key);
}
return strategy;
}
}
New behavior is added by calling register with a new key and implementation at startup, never by editing the registry's own source; the registry class itself is closed for modification but the set of strategies it serves is open for extension, which is exactly the open/closed principle applied to behavior selection.
9. Walk through a complete worked example: a checkout flow that must charge a customer using one of several payment methods (credit card, PayPal, wallet balance) selected at runtime via the Strategy pattern.
interface PaymentStrategy {
PaymentResult pay(java.math.BigDecimal amount);
}
class CreditCardStrategy implements PaymentStrategy {
public PaymentResult pay(java.math.BigDecimal amount) { return chargeCard(amount); }
private PaymentResult chargeCard(java.math.BigDecimal amount) { /* gateway call */ return PaymentResult.ok(); }
}
class PayPalStrategy implements PaymentStrategy {
public PaymentResult pay(java.math.BigDecimal amount) { /* PayPal API call */ return PaymentResult.ok(); }
}
class WalletStrategy implements PaymentStrategy {
public PaymentResult pay(java.math.BigDecimal amount) { /* debit internal wallet balance */ return PaymentResult.ok(); }
}
class CheckoutService {
PaymentResult checkout(Order order, PaymentStrategy strategy) {
return strategy.pay(order.total()); // Context delegates, unaware of which method was chosen
}
}
The controller layer resolves the right PaymentStrategy (via a factory keyed on the customer's selected method) and passes it into checkout; CheckoutService never branches on payment method at all.
10. Walk through a complete worked example: a shipping-cost calculator that supports flat-rate, weight-based, and distance-based pricing strategies selected per order — show the interface and at least two concrete strategies.
interface ShippingCostStrategy {
java.math.BigDecimal calculate(Shipment shipment);
}
class FlatRateStrategy implements ShippingCostStrategy {
public java.math.BigDecimal calculate(Shipment shipment) { return java.math.BigDecimal.valueOf(7.99); }
}
class WeightBasedStrategy implements ShippingCostStrategy {
private final java.math.BigDecimal perKg;
WeightBasedStrategy(java.math.BigDecimal perKg) { this.perKg = perKg; }
public java.math.BigDecimal calculate(Shipment shipment) {
return perKg.multiply(java.math.BigDecimal.valueOf(shipment.weightKg()));
}
}
class DistanceBasedStrategy implements ShippingCostStrategy {
public java.math.BigDecimal calculate(Shipment shipment) {
return java.math.BigDecimal.valueOf(shipment.distanceKm() * 0.15);
}
}
class OrderShippingCalculator {
java.math.BigDecimal cost(Shipment shipment, ShippingCostStrategy strategy) {
return strategy.calculate(shipment);
}
}
Which strategy applies is decided once, per order or per carrier contract, typically via a factory keyed on the shipping method the customer selected at checkout.
11. Explain why java.util.Comparator is considered the JDK's canonical real-world example of the Strategy pattern, mapping Comparator to the Strategy role and Collections.sort/List.sort's caller to the Context role.
Comparator<T> is a textbook Strategy interface: it declares one method, compare(a, b), describing the algorithm for ordering two elements, without saying anything about how the overall sort is performed. Collections.sort(list, comparator) is the Context: it owns the sorting algorithm and workflow, and delegates only the comparison decision to whatever Comparator it was given.
java.util.List<Employee> employees = getEmployees();
java.util.Collections.sort(employees, java.util.Comparator.comparing(Employee::getLastName));
java.util.Collections.sort(employees, java.util.Comparator.comparingDouble(Employee::getSalary).reversed());
Swapping the comparator changes the ordering entirely without touching the sort algorithm itself, exactly the swap-the-algorithm-not-the-caller relationship Strategy is designed around.
12. Show how Collections.sort(List, Comparator) and List.sort(Comparator) delegate the entire ordering algorithm's comparison step to an injected Strategy, and explain what stays fixed (the sort algorithm) versus what varies (the comparison logic).
List.sort internally uses a fixed, highly tuned sorting algorithm (a variant of TimSort for object arrays), and that algorithm never changes no matter which comparator is passed in. Only the pairwise comparison decision, whether one element should be considered "less than" another, is delegated out to the Comparator strategy.
employees.sort(java.util.Comparator.comparing(Employee::getDepartment)
.thenComparing(Employee::getLastName));
This split, a fixed traversal/partition algorithm with one delegated decision point, is the same shape Template Method uses for a fixed skeleton with one overridable step (see Q28-Q30); Comparator-based sorting is arguably the JDK's most visible bridge between the two ideas.
13. Demonstrate chaining multiple Comparator-based strategies using thenComparing to build a composite ordering strategy from smaller ones, and explain how this relates to the Strategy pattern's composability.
java.util.Comparator<Employee> byDeptThenSalaryThenName =
java.util.Comparator.comparing(Employee::getDepartment)
.thenComparing(Employee::getSalary, java.util.Comparator.reverseOrder())
.thenComparing(Employee::getLastName);
employees.sort(byDeptThenSalaryThenName);
Each thenComparing call wraps the previous comparator in a new comparator that only consults the next tiebreaker when the earlier one reports equality, producing a single composite Strategy instance built entirely out of smaller, independently understandable strategies rather than one large hand-written comparison method.
14. Explain how a Java lambda expression can serve as a lightweight ConcreteStrategy without ever declaring a named class, and rewrite a class-based strategy as a one-line lambda.
Any functional interface, one with a single abstract method, can be implemented inline by a lambda expression, so a stateless ConcreteStrategy that would otherwise require its own named class collapses into a single expression supplied directly at the call site.
// Before: a full class
class PercentageDiscountStrategy implements DiscountStrategy {
public java.math.BigDecimal apply(java.math.BigDecimal price) { return price.multiply(java.math.BigDecimal.valueOf(0.9)); }
}
// After: a lambda supplied directly
DiscountStrategy tenPercentOff = price -> price.multiply(java.math.BigDecimal.valueOf(0.9));
pricingEngine.priceWith(tenPercentOff);
15. Show how a method reference (e.g., String::compareToIgnoreCase or a static utility method) can be supplied directly as a Strategy implementation, and explain when a method reference is preferable to a lambda.
java.util.Comparator<String> caseInsensitive = String::compareToIgnoreCase;
names.sort(caseInsensitive);
interface TaxCalculator { java.math.BigDecimal apply(java.math.BigDecimal amount); }
TaxCalculator usSalesTax = TaxRules::calculateUsSalesTax; // static method reference
A method reference is preferable to an equivalent lambda whenever the logic already exists as a named, reusable method: it reads as a direct pointer to well-understood, independently testable code rather than an inline expression, and it avoids duplicating the same lambda body at multiple call sites.
16. What does the @FunctionalInterface annotation guarantee about a Strategy interface, and why is it good practice to annotate every Strategy interface intended to be implemented by lambdas?
@FunctionalInterface instructs the compiler to verify that the interface declares exactly one abstract method (default and static methods do not count), failing the build if a later change accidentally adds a second abstract method.
@FunctionalInterface
interface PricingStrategy {
java.math.BigDecimal price(Product product);
}
Annotating every lambda-friendly Strategy interface documents the intent for readers and protects lambda call sites across the codebase from silently breaking if someone later adds an innocuous-looking new method to the interface.
17. Walk through refactoring a legacy anonymous-inner-class Strategy implementation into a modern lambda expression, and note any behavioral differences (such as this binding) developers must watch for.
// Before: anonymous inner class
PaymentStrategy strategy = new PaymentStrategy() {
public void pay(java.math.BigDecimal amount) {
System.out.println(this); // refers to the anonymous class instance
chargeInternally(amount);
}
};
// After: lambda
PaymentStrategy strategy = amount -> {
// "this" here would refer to the ENCLOSING instance, not the lambda itself
chargeInternally(amount);
};
Inside an anonymous class, this refers to the anonymous class instance itself; inside a lambda, this refers to the enclosing instance where the lambda is written. Code that relied on this referring to the strategy object (for logging, self-reference, or equality checks) will break silently after this refactor and needs an explicit workaround.
18. Describe a scenario where a Strategy cannot be reduced to a lambda and genuinely needs a full class implementation — what characteristics make a class-based ConcreteStrategy necessary?
A class-based ConcreteStrategy is necessary when the strategy needs to hold meaningful internal state across calls (a moving average, a connection pool), implement more than one method (a multi-method Strategy interface, or an interface that also extends Comparable), or receive several constructor-injected collaborators managed by a DI container.
For example, a FraudScoringStrategy that maintains a rolling window of recent transaction amounts to detect anomalies cannot be a stateless lambda; it needs a field for that window and is naturally a class managed by Spring.
19. Why can't a Strategy interface with more than one abstract method be implemented with a lambda expression, and how would you redesign such an interface if you want lambda-based strategies?
A lambda expression can only target a functional interface with exactly one abstract method, because the lambda's body supplies the implementation for that single method and the compiler has no way to know which of several abstract methods the lambda body is meant to satisfy.
// Not lambda-friendly: two abstract methods
interface ValidationStrategy {
boolean isValid(Order order);
String errorMessage(Order order);
}
// Redesigned: one abstract method, a default derives the rest
@FunctionalInterface
interface ValidationStrategy {
ValidationResult validate(Order order); // ValidationResult carries both the flag and the message
}
20. How did the introduction of lambda expressions and functional interfaces in Java 8 change the way the Strategy pattern is typically implemented in modern codebases compared to pre-Java-8 style?
Before Java 8, every ConcreteStrategy, even a trivial one-line comparison, required a named top-level class or a verbose anonymous inner class, which discouraged fine-grained strategies and pushed developers toward fewer, larger "god" implementations. Java 8 let stateless, single-method strategies collapse into lambdas or method references supplied directly at the call site.
The GoF class-based structure has not disappeared; it remains the right choice whenever a strategy needs state, multiple collaborating methods, or dependency injection (see Q18), but modern Java code now reserves the full class hierarchy for that subset and reaches for a lambda for everything simpler.
21. Compare the intent of the Strategy pattern versus the State pattern: who decides which concrete implementation is active, and how does that differ between the two patterns?
In Strategy, the client (or a factory acting on the client's behalf) chooses the active implementation, typically once, before or at the start of an operation, and that choice rarely changes during the operation itself. In State, the active implementation changes as a natural side effect of the object's own behavior, driven by the object's internal transitions rather than by an external caller's explicit choice.
A payment method is chosen by the customer and stays fixed for that checkout (Strategy); an order's status moves from PENDING to SHIPPED to DELIVERED automatically as events occur (State).
22. Strategy and State share a nearly identical UML structure (a context holding a reference to an interface with multiple implementations) — explain precisely what structural elements are identical and what differs only in usage convention.
Structurally identical: both have a Context class holding a reference to an interface, and multiple classes implementing that interface, each representing one variant of behavior. Neither the interface, the implementing classes, nor the Context's field declaration looks any different on a class diagram.
What differs is purely behavioral convention: how often the reference changes, who changes it, and whether the implementations reference the Context back to trigger the next change (State typically does; Strategy typically does not, see Q24).
23. In the Strategy pattern, the client typically chooses (and rarely changes) the active implementation; in the State pattern, transitions happen as a side effect of the object's own operations. Give a concrete example illustrating this difference (e.g., a payment strategy vs. an order-status state machine).
// Strategy: caller picks once, no self-transition
checkoutService.checkout(order, new PayPalStrategy());
// State: the object transitions itself as a side effect of its own method
class Order {
private OrderState state = new PendingState();
void markShipped() { state = state.ship(this); } // state decides and returns the next state
}
No caller ever tells Order which state class to use next; the current state's own method decides the transition. Compare this to PayPalStrategy, which never decides to become WalletStrategy on its own.
24. Do ConcreteState implementations typically hold a back-reference to the Context so they can trigger transitions, and how does this differ from how ConcreteStrategy implementations relate to their Context?
Yes: a ConcreteState implementation is commonly handed the Context (or receives it as a parameter on its transition methods) precisely so it can call back into the Context to install the next state, since transitions are the state's own responsibility.
A ConcreteStrategy, by contrast, generally has no reference to its Context at all and no mechanism to change which strategy is active; it simply performs its algorithm on the arguments it is given and returns a result, remaining decoupled from whatever object is using it.
25. If two developers on a team are debating whether a given class is "really" Strategy or State pattern given identical code structure, how would you help them resolve the disagreement by focusing on intent rather than structure?
Ask three questions: who selects the initial implementation and how often does it change afterward, does the change happen as a direct side effect of one of the object's own method calls, and would renaming the interface from "...Strategy" to "...State" change anyone's mental model of when to add a new implementation.
If the answer is "the caller picks once and it almost never changes," it is Strategy; if "the object cycles through variants on its own as things happen to it," it is State. The debate is not about the code shape, which is identical either way, but about the story the code is telling.
26. Walk through refactoring a State-pattern order-status implementation into a Strategy-pattern shipping-method implementation (or vice versa) purely to illustrate how the same skeleton code serves two different design intents.
// State-shaped skeleton
interface OrderState { OrderState next(Order order); }
class PendingState implements OrderState {
public OrderState next(Order order) { return new ShippedState(); }
}
// Identical skeleton, reused with Strategy intent: interface renamed, no self-transition
interface ShippingMethodStrategy { java.math.BigDecimal cost(Shipment shipment); }
class ExpressShippingStrategy implements ShippingMethodStrategy {
public java.math.BigDecimal cost(Shipment shipment) { return java.math.BigDecimal.valueOf(24.99); }
}
The mechanical refactor is trivial, delete the self-transition method and any back-reference to the owning object; the meaningful change is entirely about who is now responsible for selecting the implementation.
27. Describe a real bug that occurred because a team implemented something structurally as Strategy but the actual requirement was State (transitions happening automatically) — what went wrong and how was it fixed?
A team modeled subscription billing as a BillingStrategy chosen once at signup (TrialStrategy, then later manually swapped to PaidStrategy). The bug: nothing in the code automatically transitioned a trial to paid billing when the trial period expired, because Strategy implementations never trigger their own replacement, so expired trials kept billing as trials indefinitely until a batch job happened to notice, sometimes days late.
The fix reframed billing status as a State machine: each state's own tick() method checked whether its condition (trial expiry) had been met and returned the next state, so the transition happened automatically as a side effect of a scheduled check rather than relying on an external caller to remember to swap the strategy.
28. Compare the Strategy pattern to the Template Method pattern: which pattern varies an entire algorithm through composition, and which varies a single step of a fixed algorithm through inheritance?
Strategy varies the entire algorithm: the Context delegates the whole operation to a composed, injected object, and different strategies can look completely different internally as long as they satisfy the same interface. Template Method keeps the overall algorithm's skeleton fixed in a base class and varies only one or a few steps, which subclasses override through inheritance.
// Template Method: fixed skeleton, one varying step via inheritance
abstract class ReportGenerator {
final void generate() { fetchData(); format(); } // fixed skeleton
abstract void format(); // varies per subclass
private void fetchData() { /* shared, fixed */ }
}
// Strategy: entire algorithm swapped via composition
class ReportContext {
private final FormatStrategy strategy;
ReportContext(FormatStrategy strategy) { this.strategy = strategy; }
void generate() { strategy.format(fetchData()); }
}
29. Explain how the choice between Strategy and Template Method is often phrased as "favor composition over inheritance" — why does Strategy embody that principle and Template Method push against it?
Strategy embodies composition: variation is achieved by plugging a different object in at runtime, with no inheritance relationship required between the Context and the strategies at all. Template Method requires inheritance by construction: a subclass must extend the abstract base class to supply the varying step, binding the variation to the class hierarchy at compile time.
"Favor composition over inheritance" recommends Strategy specifically because composed behavior can be swapped per instance, combined freely, and unit-tested independently of any base class, whereas inheritance-based variation is fixed once compiled and cannot be reassigned to an existing object at runtime.
30. In a Template Method implementation, describe what a "hook" method is, and contrast it with how Strategy fully replaces the varying behavior rather than merely customizing one step.
A hook is an optional, often no-op, method in a Template Method base class that a subclass may override to influence one small point in the fixed algorithm (whether to send a notification, whether to log verbosely) without needing to override the main varying step at all.
abstract class ReportGenerator {
final void generate() {
fetchData();
format();
if (shouldNotify()) notifySubscribers(); // hook: subclasses may override
}
protected boolean shouldNotify() { return false; } // default hook implementation
abstract void format();
}
Strategy has no equivalent notion of a partial hook: a ConcreteStrategy either fully implements the delegated operation or is not a valid strategy at all, since the Context delegates the entire operation rather than one optional customization point within a larger fixed sequence.
31. Show how Strategy and Template Method can be combined: a template method's one variable step is implemented by delegating to an injected Strategy rather than requiring a subclass override.
class ReportGenerator {
private final FormatStrategy formatStrategy; // the "varying step" is now composed, not overridden
ReportGenerator(FormatStrategy formatStrategy) { this.formatStrategy = formatStrategy; }
final String generate() {
RawData data = fetchData(); // fixed
return formatStrategy.format(data); // delegated instead of subclassed
}
private RawData fetchData() { /* shared, fixed */ return new RawData(); }
}
This avoids creating a new subclass per output format, since the fixed skeleton now lives in a single concrete class and each format variation is just another FormatStrategy instance, combining Template Method's fixed-skeleton benefit with Strategy's runtime-swappable composition.
32. Give practical guidance for choosing between Strategy and Template Method when designing a new piece of behavior-varying code: what questions should you ask about how many steps vary and whether subclassing or composition is more appropriate?
Ask how many steps of the algorithm actually vary: if it is genuinely one entire operation that varies as a whole, Strategy's composition fits naturally. If most of the algorithm is fixed and shared, and only one or two well-defined steps vary, Template Method's inheritance-based hooks avoid re-stating the shared skeleton in every variant.
Also ask whether the variation must be assignable to an existing object at runtime (favoring Strategy) or is fully known and fixed at the point an object is created (where Template Method's simplicity is not a liability), and whether you expect many independent combinations of variations, which composition handles far more gracefully than a subclass-per-combination explosion.
33. What rigidity does Template Method's inheritance-based design impose that Strategy's composition-based design avoids, particularly around combining multiple variations or changing behavior at runtime?
Because a subclass can only extend one base class, Template Method cannot easily combine two independent variations without either multiple inheritance (unavailable in Java) or a combinatorial subclass explosion, one subclass per combination of variations. It also cannot reassign an existing object's behavior after construction; the variant is baked in by which subclass was instantiated.
Strategy avoids both problems: a Context can hold several independent Strategy fields, each varied separately without multiplying classes, and any Strategy field can be swapped on an existing instance (via setter injection, see Q6) without recreating the object.
34. Walk through refactoring a Template Method class hierarchy that has grown an unwieldy number of subclasses (one per variation) into a Strategy-based design with a single Context class and injected strategies.
// Before: one subclass per report format, growing without bound
abstract class ReportGenerator { abstract void format(RawData data); }
class PdfReportGenerator extends ReportGenerator { void format(RawData data) { /* pdf */ } }
class CsvReportGenerator extends ReportGenerator { void format(RawData data) { /* csv */ } }
class XlsxReportGenerator extends ReportGenerator { void format(RawData data) { /* xlsx */ } }
// After: one Context class, format extracted to an injected Strategy
class ReportGenerator {
private final FormatStrategy formatStrategy;
ReportGenerator(FormatStrategy formatStrategy) { this.formatStrategy = formatStrategy; }
String generate(RawData data) { return formatStrategy.format(data); }
}
Each old subclass becomes a small FormatStrategy implementation (or a lambda, if stateless); callers switch from new PdfReportGenerator() to new ReportGenerator(pdfFormatStrategy), and adding a new format no longer means adding a new subclass of the Context itself.
35. Compare the Strategy pattern to the Bridge pattern: both use composition to decouple two things, so what exactly differs between the abstraction/implementation split in Bridge and the algorithm swap in Strategy?
Strategy decouples a single interchangeable algorithm from the one class that uses it, and the "strategy" side is usually a single-purpose, narrow interface. Bridge deliberately splits an entire abstraction hierarchy (multiple related classes with their own inheritance) from an entire implementation hierarchy, so both sides can independently grow their own family of subclasses without multiplying combinations.
Strategy typically has one Context and many interchangeable strategies; Bridge typically has a hierarchy of abstractions, each of which can be paired with any implementation from a separate hierarchy of implementations.
36. Explain how Bridge deliberately separates an abstraction hierarchy from an implementation hierarchy so both can vary independently, and why this differs from Strategy's simpler "one interface, many interchangeable algorithms" shape.
// Bridge: an abstraction hierarchy composed with a separate implementation hierarchy
abstract class Shape {
protected final Renderer renderer; // the "implementation" side, injected
Shape(Renderer renderer) { this.renderer = renderer; }
abstract void draw();
}
class Circle extends Shape { void draw() { renderer.renderCircle(); } }
class Square extends Shape { void draw() { renderer.renderSquare(); } }
// Renderer has its own hierarchy: VectorRenderer, RasterRenderer, ...
Bridge is designed proactively so Circle/Square and VectorRenderer/RasterRenderer can each grow independently; Strategy's shape is simpler because there is usually only one abstraction (the Context) being composed with one family of interchangeable algorithms, not two independently varying hierarchies.
37. Both Strategy and Bridge hold a reference to another interface and delegate to it — what practical clue helps you tell, when reading unfamiliar code, whether the author intended Bridge or Strategy?
Check whether the class holding the reference is itself part of a growing subclass hierarchy. If the composing class has several subclasses of its own, and each subclass could sensibly be paired with any of several implementations of the composed interface, that combinatorial two-hierarchies shape signals Bridge.
If the composing class is a single, non-hierarchical Context whose only variation point is which single strategy object it was handed, that simpler one-sided shape signals Strategy. When in doubt, ask whether the design was proactively split at design time to let two things vary together (Bridge) or reactively assembled to make one behavior swappable (Strategy).
38. Describe a scenario (e.g., a UI toolkit rendering across platforms, or a reporting abstraction with pluggable output formats) where Bridge is the more appropriate choice over Strategy, and explain why.
A cross-platform UI toolkit with several widget types (Button, Checkbox, Slider) that must each render correctly on several platforms (Windows, macOS, a web canvas) is a Bridge scenario: without Bridge you would need a subclass per widget-per-platform combination, an explosion Bridge avoids by composing a small widget hierarchy with a small, independently varying rendering-implementation hierarchy.
Strategy would be the wrong tool here because it does not anticipate a whole family of composing classes (the widgets) needing to pair with a whole family of implementations (the renderers); Strategy assumes one Context, not a hierarchy of them.
39. Compare the Strategy pattern to the Command pattern: both encapsulate behavior behind an interface, so what differs about what each interface represents and how the encapsulated behavior is used?
A Strategy interface represents an interchangeable way to perform one ongoing operation the Context needs (how to sort, how to price), and the Context calls it directly, synchronously, as part of its own logic. A Command interface represents a fully self-contained request to perform an action later, encapsulating both the action and its receiver, so it can be queued, logged, scheduled, or undone independently of whoever created it.
The practical difference: you rarely store a list of pending strategies waiting to be invoked, but storing a queue of pending commands to execute in order is exactly Command's typical use case.
40. Command objects typically support undo, queuing, and logging of the requested operation — explain why these concerns rarely apply to a Strategy implementation, and what that implies about each pattern's typical interface shape (parameterless execute() vs a strategy method that takes the operands directly).
// Command: parameterless execute(), receiver and arguments captured internally
interface Command { void execute(); void undo(); }
class MoveShapeCommand implements Command {
private final Shape shape; private final int dx, dy;
public void execute() { shape.moveBy(dx, dy); }
public void undo() { shape.moveBy(-dx, -dy); }
}
// Strategy: operands passed directly at call time, no undo concept
interface PaymentStrategy { void pay(java.math.BigDecimal amount); }
Strategy's operands (the amount, the record to sort) are supplied fresh on each call rather than captured ahead of time, so there is no natural "undo" of a comparison or a payment calculation choice; Command's whole reason for existing is to capture a request as data precisely so it can be replayed, logged, or reversed later.
41. Some codebases implement a single-method interface with an implementing class per behavior and call it "Command" while another calls the identical shape "Strategy" — clarify the naming distinction based on intent (executing a request as a first-class object vs. selecting an algorithm).
Both can look identical, a single abstract method, several implementing classes, and a caller holding a reference. The distinguishing question is what the object represents: if it represents "a specific request to be carried out," including which receiver and which arguments, name it and treat it as a Command. If it represents "one of several ways to compute the same kind of result," name it and treat it as a Strategy.
A misnamed abstraction is not a bug by itself, but naming it correctly signals to future maintainers whether they should expect queuing/undo semantics (Command) or simple runtime substitutability (Strategy).
42. Describe how Strategy and Command can be combined in a single system, for example a Context selecting among several pricing Strategies, each of which is itself invoked via a queued Command for asynchronous processing.
class RepriceCommand implements Command {
private final Order order;
private final PricingStrategy strategy; // Command wraps a Strategy choice
RepriceCommand(Order order, PricingStrategy strategy) { this.order = order; this.strategy = strategy; }
public void execute() {
order.setTotal(strategy.price(order)); // Strategy performs the actual computation
}
}
commandQueue.submit(new RepriceCommand(order, seasonalDiscountStrategy));
The Command captures "reprice this specific order later, using this specific pricing algorithm," queuing the whole unit of work for asynchronous execution, while the Strategy inside it remains a pure, reusable pricing algorithm with no knowledge of queuing at all.
43. Show how Spring's @Qualifier annotation lets you select a specific Strategy bean implementation by name when multiple beans implement the same Strategy interface.
@Component("creditCardStrategy")
class CreditCardStrategy implements PaymentStrategy { /* ... */ }
@Component("payPalStrategy")
class PayPalStrategy implements PaymentStrategy { /* ... */ }
@Service
class CheckoutService {
private final PaymentStrategy strategy;
CheckoutService(@Qualifier("payPalStrategy") PaymentStrategy strategy) {
this.strategy = strategy;
}
}
@Qualifier resolves the ambiguity Spring would otherwise raise when several beans satisfy the same interface type, letting you pin a specific Context to a specific strategy bean by its registered name rather than relying on injection order or a primary bean.
44. Demonstrate autowiring a Map<String, Strategy> in Spring, where Spring automatically populates the map with every bean implementing the Strategy interface keyed by bean name, and explain how a Context can use this map for runtime dispatch.
@Service
class PaymentDispatcher {
private final java.util.Map<String, PaymentStrategy> strategies;
PaymentDispatcher(java.util.Map<String, PaymentStrategy> strategies) {
this.strategies = strategies; // Spring injects every PaymentStrategy bean, keyed by bean name
}
PaymentResult pay(String method, java.math.BigDecimal amount) {
PaymentStrategy strategy = strategies.get(method);
if (strategy == null) throw new IllegalArgumentException("Unknown method: " + method);
return strategy.pay(amount);
}
}
This gives you a runtime dispatch table for free, driven entirely by Spring's component scanning, with no manual registry code and no switch statement to update when a new payment method bean is added.
45. Explain how @ConditionalOnProperty can be used to register only the Strategy bean appropriate for the current environment (e.g., a MockPaymentStrategy in dev, a RealPaymentStrategy in prod), and why this differs from selecting a strategy at request time.
@Component
@ConditionalOnProperty(name = "payments.mode", havingValue = "mock")
class MockPaymentStrategy implements PaymentStrategy { /* no real charge */ }
@Component
@ConditionalOnProperty(name = "payments.mode", havingValue = "live", matchIfMissing = true)
class LivePaymentStrategy implements PaymentStrategy { /* real gateway call */ }
Only one of these beans is ever created per running application instance, based on configuration evaluated once at startup; this is fundamentally different from a per-request strategy lookup (see Q44), which chooses among several simultaneously-registered beans for each individual call.
46. Design a StrategyResolver service in a Spring application that looks up the correct Strategy bean at runtime based on a request parameter, insulating controllers from Spring's DI machinery.
@Service
class PaymentStrategyResolver {
private final java.util.Map<String, PaymentStrategy> strategiesByKey;
PaymentStrategyResolver(java.util.List<PaymentStrategy> strategies) {
this.strategiesByKey = strategies.stream()
.collect(java.util.stream.Collectors.toMap(s -> s.getClass().getSimpleName(), s -> s));
}
PaymentStrategy resolve(String method) {
return strategiesByKey.values().stream()
.filter(s -> s.supports(method))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Unsupported method: " + method));
}
}
Controllers depend only on PaymentStrategyResolver.resolve(method), never on Spring's ApplicationContext, bean names, or qualifiers directly, keeping the resolution logic testable and centralized in one place.
47. What pitfall can @Primary create when multiple Strategy beans exist, and why can silently picking a "primary" bean mask a configuration mistake that should have been an explicit, visible choice?
@Primary tells Spring "if nothing else disambiguates, use this bean," which resolves ambiguous injection points silently and successfully, even when the injection point genuinely needed a specific, different strategy and the developer simply forgot a qualifier.
Because the application still starts and runs without error, this class of bug tends to surface only in production as subtly wrong behavior (the wrong payment method always being used, say) rather than as a build-time or startup failure, making it considerably harder to trace than an explicit NoUniqueBeanDefinitionException would have been.
@Primary for cases where one implementation is genuinely the sensible default for every consumer; prefer explicit @Qualifier everywhere the choice actually matters per call site.48. What error does Spring throw when two beans implement the same Strategy interface and no qualifier or primary is specified, and how do you resolve it cleanly?
// org.springframework.beans.factory.NoUniqueBeanDefinitionException:
// expected single matching bean but found 2: creditCardStrategy, payPalStrategy
Spring throws NoUniqueBeanDefinitionException at context startup because it cannot decide which of the matching beans to inject. The clean fix depends on the actual need: add an explicit @Qualifier at each injection point that needs one specific strategy, or, if the Context genuinely needs every implementation, switch the injection point's type to List<PaymentStrategy> or Map<String, PaymentStrategy> instead of a single bean (see Q44).
49. Show how to test a Spring-managed Context class in isolation using @MockBean to replace its injected Strategy dependency.
@SpringBootTest
class CheckoutServiceTest {
@MockBean PaymentStrategy paymentStrategy;
@Autowired CheckoutService checkoutService;
@Test
void delegatesToInjectedStrategy() {
when(paymentStrategy.pay(any())).thenReturn(PaymentResult.ok());
checkoutService.checkout(sampleOrder(), paymentStrategy);
verify(paymentStrategy).pay(sampleOrder().total());
}
}
@MockBean replaces the real bean in the Spring context with a Mockito mock for the duration of the test, letting you verify the Context's delegation behavior without invoking any real strategy implementation or its external dependencies.
50. What naming convention for Strategy bean names (or @Qualifier values) helps keep a large registry of strategies maintainable as the codebase grows, and why should the bean name match a stable, versionable configuration key rather than the class name itself?
Name strategy beans after a stable business key ("CREDIT_CARD", "GOLD_TIER") rather than after the implementation class name ("creditCardStrategyImplV2"), because the business key is what appears in configuration files, database rows, and API payloads, and it should not need to change just because the class implementing it was renamed or refactored.
Tying bean names to class names instead means renaming a class for an unrelated reason silently breaks every configuration value that referenced the old bean name, a maintenance trap that is easy to overlook since the compiler cannot catch a string mismatch.
51. Explain how to unit test that a Context correctly calls its injected Strategy with the right arguments, using a mock of the Strategy interface — what exactly should the test assert?
@Test
void passesOrderTotalToStrategy() {
PaymentStrategy strategy = mock(PaymentStrategy.class);
CheckoutService service = new CheckoutService();
Order order = new Order(java.math.BigDecimal.valueOf(49.99));
service.checkout(order, strategy);
verify(strategy).pay(java.math.BigDecimal.valueOf(49.99));
}
The test should assert that the Context calls the strategy exactly once, with the correctly derived arguments (not the raw order object, if the interface expects a computed amount), and that the Context correctly returns or forwards whatever the mocked strategy returns, without asserting anything about how the real strategy computes its result.
52. Describe an approach for testing each ConcreteStrategy implementation in isolation, independent of any Context, and why this is usually simpler and more valuable than testing through the Context.
@Test
void weightBasedStrategyChargesPerKilogram() {
WeightBasedStrategy strategy = new WeightBasedStrategy(java.math.BigDecimal.valueOf(2.50));
java.math.BigDecimal cost = strategy.calculate(new Shipment(4.0, 0));
assertThat(cost).isEqualByComparingTo("10.00");
}
Testing each strategy directly, constructing it and calling its method with representative inputs, is simpler because there is no Context wiring or mocking overhead, and it is more valuable because it exercises the actual algorithm's correctness rather than merely confirming delegation, which the Context tests already cover separately (see Q51).
53. What is a "contract test suite," and how would you write one shared test suite that every ConcreteStrategy implementation must pass, ensuring behavioral consistency across implementations of the same interface?
abstract class PaymentStrategyContractTest {
protected abstract PaymentStrategy createStrategy();
@Test
void rejectsNegativeAmounts() {
PaymentStrategy strategy = createStrategy();
assertThrows(IllegalArgumentException.class, () -> strategy.pay(java.math.BigDecimal.valueOf(-1)));
}
}
class CreditCardStrategyTest extends PaymentStrategyContractTest {
protected PaymentStrategy createStrategy() { return new CreditCardStrategy(); }
}
A contract test suite is a shared abstract test class encoding invariants every implementation of an interface must satisfy (rejecting invalid input, being idempotent, returning a non-null result); each concrete implementation's own test class extends it and only supplies its own instance, guaranteeing every strategy is checked against the same baseline rules automatically.
54. How would you mock a Strategy interface annotated with @FunctionalInterface using Mockito, and what subtlety arises if the interface has default methods you want to exercise for real?
PaymentStrategy strategy = mock(PaymentStrategy.class);
when(strategy.pay(any())).thenReturn(PaymentResult.ok());
// Exercise a default method's real logic instead of stubbing it away
PaymentStrategy strategy2 = mock(PaymentStrategy.class, org.mockito.Mockito.CALLS_REAL_METHODS);
A plain mock() stubs every method, including default methods, to return Mockito's default value (typically null or a default primitive) unless explicitly stubbed; if you actually want a default method's real logic to run during the test, use CALLS_REAL_METHODS, or wrap a real lambda instance instead of a Mockito mock.
55. Explain the difference between an integration test that exercises a Context wired to its real production Strategy versus a unit test that exercises the Context with a mocked Strategy — when is each appropriate?
A unit test with a mocked Strategy isolates the Context's own logic (argument derivation, result handling, error propagation) from the strategy's real computation, runs fast, and pinpoints failures precisely to the Context. An integration test wiring the real Strategy verifies that the Context and a specific real implementation actually cooperate correctly end to end, including any Spring wiring, external calls, or configuration involved.
Use unit tests as the default for every Context and every ConcreteStrategy in isolation; reserve integration tests for the handful of critical paths (an actual payment gateway call, a real sort of production-shaped data) where the wiring itself, not just each piece separately, needs verification.
56. Show how to write a parameterized test (JUnit 5 @ParameterizedTest) that runs the same assertions across every registered ConcreteStrategy implementation of an interface.
@ParameterizedTest
@MethodSource("allStrategies")
void everyStrategyReturnsNonNegativeCost(ShippingCostStrategy strategy) {
java.math.BigDecimal cost = strategy.calculate(sampleShipment());
assertThat(cost).isGreaterThanOrEqualTo(java.math.BigDecimal.ZERO);
}
static java.util.stream.Stream<ShippingCostStrategy> allStrategies() {
return java.util.stream.Stream.of(
new FlatRateStrategy(), new WeightBasedStrategy(java.math.BigDecimal.ONE), new DistanceBasedStrategy());
}
This runs one shared invariant check against every current and future strategy supplied by allStrategies(), catching a newly added implementation that violates a basic assumption without anyone needing to remember to add a bespoke test for it.
57. Why should a shared Strategy instance, reused across many Context instances and potentially many threads, be stateless or otherwise safe for concurrent use, and what Java techniques (immutability, thread confinement, ThreadLocal) help guarantee this?
A single Strategy instance is commonly shared across many Context objects and many concurrent requests, for efficiency; if it holds mutable per-call state in an instance field rather than a local variable, concurrent invocations will read and overwrite each other's data (see Q58 for a concrete failure).
// Safe: all state is local to the method call, nothing shared between threads
class WeightBasedStrategy implements ShippingCostStrategy {
private final java.math.BigDecimal perKg; // immutable configuration, fine to share
public java.math.BigDecimal calculate(Shipment shipment) {
java.math.BigDecimal weight = java.math.BigDecimal.valueOf(shipment.weightKg()); // local, not shared
return perKg.multiply(weight);
}
}
Prefer immutable fields set once at construction, keep any working state as local method variables rather than instance fields, and reach for ThreadLocal only as a last resort for genuinely thread-specific caches, since it adds cleanup responsibilities of its own.
58. Describe a real bug caused by a stateful Strategy implementation being incorrectly shared as a singleton across concurrent requests — walk through what state was mutated, what symptom appeared in production, and how it was fixed.
// Buggy: mutable instance field shared across every concurrent request
@Component
class RunningTotalDiscountStrategy implements DiscountStrategy {
private java.math.BigDecimal totalDiscounted = java.math.BigDecimal.ZERO; // BUG: shared mutable state
public java.math.BigDecimal apply(java.math.BigDecimal price) {
java.math.BigDecimal discount = price.multiply(java.math.BigDecimal.valueOf(0.1));
totalDiscounted = totalDiscounted.add(discount); // corrupted by concurrent requests
return price.subtract(discount);
}
}
Registered as a default-scope (singleton) Spring bean, this strategy's totalDiscounted field was mutated by every concurrent checkout request simultaneously, so the value one request read was frequently overwritten mid-calculation by another request's write, and totals reported to a nightly reconciliation job drifted from actual charges under load.
The fix removed the shared mutable field entirely, since the running total belonged in a database or per-request context rather than the strategy instance, restoring the strategy to genuinely stateless, safely-shared behavior.
59. When is it safe to treat a Strategy implementation as an application-wide singleton reused by every Context, and what property of the strategy's implementation makes this safe?
It is safe exactly when the strategy is stateless, or holds only immutable configuration set once at construction and never mutated afterward; in that case every field read during a call is either immutable or a fresh local variable, so concurrent invocations cannot interfere with one another.
The moment a strategy needs to remember anything between calls that varies per caller or per request, that state must live outside the shared instance, in a per-request object, a database, or a cache explicitly keyed by request identity, rather than in an instance field of the shared singleton.
60. What is the actual runtime performance cost, if any, of dispatching through a Strategy interface (virtual/interface method call) compared to a hardcoded if/else or switch statement choosing behavior inline?
For a hot, frequently-called call site with a small, stable set of implementing classes, the cost is typically negligible: the JIT compiler routinely inlines and devirtualizes interface calls once it has established which concrete type is actually flowing through (see Q61). The measurable difference between an interface dispatch and an inline branch usually only appears in extremely tight, high-iteration loops.
The bigger practical costs of the Strategy pattern, if any, tend to come from what strategy implementations do internally (object allocation, I/O, reflection) rather than from the dispatch mechanism itself, so profile before assuming Strategy has introduced a measurable slowdown.
61. Explain how a JIT compiler's monomorphic call-site optimization can make dispatch through a Strategy interface nearly as fast as a direct method call, and under what conditions (megamorphic call sites) this optimization stops applying.
When a particular call site (a specific line of code calling strategy.pay(...)) is observed to always invoke the same concrete implementation, a modern JIT (such as HotSpot's C2 compiler) can speculatively inline that one implementation directly and skip the virtual dispatch entirely, a technique called monomorphic inline caching.
If that same call site later starts seeing several different concrete implementations flow through it interchangeably (a megamorphic call site), the JIT can no longer safely speculate on one target and falls back to a real virtual dispatch for every call, losing the inlining benefit. This is one reason a Context that always uses the same strategy instance for its lifetime tends to be faster than one that swaps strategies constantly on a hot path.
62. Describe how you would use a microbenchmark tool such as JMH to measure the real overhead of Strategy-based dispatch versus inline conditional logic in a performance-critical path, and why naive System.currentTimeMillis() timing is unreliable for this.
@Benchmark
public java.math.BigDecimal viaStrategy() { return strategy.calculate(shipment); }
@Benchmark
public java.math.BigDecimal viaInlineSwitch() {
return switch (kind) {
case FLAT -> java.math.BigDecimal.valueOf(7.99);
case WEIGHT -> perKg.multiply(java.math.BigDecimal.valueOf(shipment.weightKg()));
};
}
JMH runs many warm-up iterations so the JIT has fully optimized both code paths before measurement begins, and it accounts for dead-code elimination and constant folding that would otherwise let the JIT optimize away work a naive hand-timed loop never actually performs. Manual System.currentTimeMillis() timing typically measures cold, unoptimized bytecode, wildly overstating any real difference between the two approaches once both are running hot in production.
63. Design a discount/pricing engine that selects among multiple discount Strategy implementations at runtime based on a customer's loyalty tier — show the interface and how the Context chooses the right strategy.
interface DiscountStrategy {
java.math.BigDecimal apply(java.math.BigDecimal price);
}
class BronzeDiscountStrategy implements DiscountStrategy {
public java.math.BigDecimal apply(java.math.BigDecimal price) { return price; } // no discount
}
class GoldDiscountStrategy implements DiscountStrategy {
public java.math.BigDecimal apply(java.math.BigDecimal price) { return price.multiply(java.math.BigDecimal.valueOf(0.9)); }
}
class PlatinumDiscountStrategy implements DiscountStrategy {
public java.math.BigDecimal apply(java.math.BigDecimal price) { return price.multiply(java.math.BigDecimal.valueOf(0.8)); }
}
class PricingEngine {
private final java.util.Map<CustomerTier, DiscountStrategy> byTier;
PricingEngine(java.util.Map<CustomerTier, DiscountStrategy> byTier) { this.byTier = byTier; }
java.math.BigDecimal priceFor(Customer customer, java.math.BigDecimal price) {
return byTier.get(customer.tier()).apply(price);
}
}
64. In the discount engine from the previous question, how would you support combining multiple discounts (e.g., a tier discount and a seasonal promotion) without collapsing back into a single "God Strategy" that branches internally on every combination?
class CompositeDiscountStrategy implements DiscountStrategy {
private final java.util.List<DiscountStrategy> strategies;
CompositeDiscountStrategy(java.util.List<DiscountStrategy> strategies) { this.strategies = strategies; }
public java.math.BigDecimal apply(java.math.BigDecimal price) {
java.math.BigDecimal result = price;
for (DiscountStrategy strategy : strategies) {
result = strategy.apply(result); // each strategy applies to the running result
}
return result;
}
}
PricingEngine.priceFor(customer, price, new CompositeDiscountStrategy(
java.util.List.of(goldDiscountStrategy, seasonalPromotionStrategy)));
A CompositeDiscountStrategy is itself just another DiscountStrategy that delegates to a list of others in sequence, letting you combine any subset of discounts declaratively rather than writing a new branch for every possible combination (see Q68).
65. What risk arises when discount strategies are naively stacked (each strategy assumes it is applied to the original, undiscounted price) and how would you design the pricing engine to apply them safely and predictably?
If each strategy independently computes its discount off the original list price rather than the running, already-discounted price, stacking two strategies naively (by summing their individual discount amounts) can produce a total discount that does not match applying them sequentially, and in the worst case can discount below cost or below zero.
// Risky: summing independent percentage discounts off the original price can overshoot
java.math.BigDecimal combined = price
.subtract(price.multiply(java.math.BigDecimal.valueOf(0.10))) // gold: -10%
.subtract(price.multiply(java.math.BigDecimal.valueOf(0.15))); // seasonal: -15% of ORIGINAL, not sequential
// Safer: apply sequentially against the running result (see Q64), and clamp the floor
result = result.max(java.math.BigDecimal.ZERO);
Design the composite to apply strategies sequentially against the running result, document explicitly whether percentage discounts stack multiplicatively or additively, and always clamp the final price at a sane floor.
66. How would you allow a discount/pricing engine to switch strategies at runtime based on a live configuration change (a feature flag or admin toggle) without restarting the application or losing in-flight requests using the old strategy?
Hold the active strategy behind an AtomicReference (or a volatile field) rather than a plain field, and have the configuration-change listener swap the reference to a new strategy instance; any in-flight call that already read the old reference completes using it, since references are not mutated mid-call, only reassigned for subsequent calls.
class PricingEngine {
private final java.util.concurrent.atomic.AtomicReference<DiscountStrategy> active;
void onConfigChange(DiscountStrategy newStrategy) { active.set(newStrategy); }
java.math.BigDecimal priceFor(java.math.BigDecimal price) { return active.get().apply(price); }
}
67. What are the trade-offs of caching the result of a strategy-selection decision (e.g., which discount strategy applies to a given customer) versus re-evaluating the selection logic on every request?
Caching the resolved strategy per customer (or per session) avoids repeating potentially expensive selection logic (a database lookup for tier, a feature-flag evaluation) on every single request, but risks serving a stale strategy if the customer's tier or the active configuration changes before the cache entry expires or is invalidated.
Re-evaluating on every request guarantees correctness against the latest configuration at the cost of repeating the selection work each time; a middle ground caches the selection with a short time-to-live or an explicit invalidation hook tied to the events (a tier upgrade, a config change) that would make the cached choice stale.
68. Describe how a "composite Strategy" — a ConcreteStrategy that itself holds and delegates to a list of other Strategies — differs from the anti-pattern of a single Strategy that internally branches on a type flag.
A composite Strategy still fully honors the pattern's intent: it implements the same interface as its children, delegates entirely to them, and can be extended with a new child strategy without modifying the composite's own source code. A "God Strategy" that internally checks if (type == GOLD) ... else if (type == PLATINUM) ... has smuggled the very conditional the pattern exists to eliminate back inside a single class (see Q71), so adding a new tier means editing that class instead of adding a new implementation.
69. How would you design a system that runs two different pricing Strategy implementations side by side for a fraction of live traffic (an A/B test) and safely measures which produces better outcomes?
class ExperimentPricingStrategy implements DiscountStrategy {
private final DiscountStrategy control, variant;
private final ExperimentBucketer bucketer;
public java.math.BigDecimal apply(java.math.BigDecimal price) {
// selection happens once per stable customer id, not per call, to avoid flip-flopping mid-session
return bucketer.isInVariant() ? variant.apply(price) : control.apply(price);
}
}
Bucket customers into control or variant consistently (typically hashing a stable customer ID), record which strategy priced each transaction alongside the outcome (conversion, revenue), and analyze the two cohorts independently; the Strategy pattern keeps both pricing algorithms fully interchangeable and equally testable in isolation before the experiment even starts.
70. Show how a feature flag service can be consulted inside a Strategy factory to decide which ConcreteStrategy implementation to hand back for a given user or request, without scattering feature-flag checks throughout the Context.
class DiscountStrategyFactory {
private final FeatureFlagService flags;
DiscountStrategyFactory(FeatureFlagService flags) { this.flags = flags; }
DiscountStrategy strategyFor(Customer customer) {
if (flags.isEnabled("new-tier-pricing", customer.id())) {
return new RevisedTierDiscountStrategy();
}
return new LegacyTierDiscountStrategy();
}
}
Every feature-flag check lives in exactly one factory method; the PricingEngine Context and every other caller only ever see a resolved DiscountStrategy instance and never need to know a flag was involved in choosing it.
71. What is the "God Strategy" anti-pattern, and why does a single ConcreteStrategy implementation that internally branches on a type or flag parameter defeat the entire purpose of using the Strategy pattern?
// Anti-pattern: one "strategy" that still branches internally
class GodDiscountStrategy implements DiscountStrategy {
private final CustomerTier tier;
public java.math.BigDecimal apply(java.math.BigDecimal price) {
if (tier == CustomerTier.GOLD) return price.multiply(java.math.BigDecimal.valueOf(0.9));
else if (tier == CustomerTier.PLATINUM) return price.multiply(java.math.BigDecimal.valueOf(0.8));
else return price;
}
}
This "strategy" is really just the original conditional chain wrapped in a class that implements the interface; adding a new tier still requires editing this one class's source, exactly the coupling Strategy exists to remove. A correct design would have one small class per tier (see Q63), each holding no knowledge of any other tier at all.
72. Describe a production bug where a Context was constructed with a null Strategy reference, and calling a method on it threw a NullPointerException deep in unrelated code — how would you redesign the Context to fail fast at construction time instead?
// Buggy: no validation, NPE surfaces far from the real mistake
class CheckoutContext {
private final PaymentStrategy strategy;
CheckoutContext(PaymentStrategy strategy) { this.strategy = strategy; } // null accepted silently
void checkout(java.math.BigDecimal total) { strategy.pay(total); } // NPE happens here, minutes later
}
// Fixed: fail fast, at construction, with a clear message
class CheckoutContext {
private final PaymentStrategy strategy;
CheckoutContext(PaymentStrategy strategy) {
this.strategy = java.util.Objects.requireNonNull(strategy, "strategy must not be null");
}
}
A factory method upstream returned null for an unrecognized payment method instead of throwing, and the resulting CheckoutContext was constructed successfully, only failing minutes later, on a completely unrelated request, when checkout() was finally called — far from where the actual mistake occurred, making the root cause much harder to trace.
73. Why is validating a non-null Strategy in the Context's constructor (fail-fast) preferable to discovering the missing strategy only when the delegated method is first called at runtime?
Failing at construction time puts the exception at the exact place and moment the mistake was actually made (a null was handed to this constructor), with a stack trace pointing directly at the caller responsible, rather than at some unrelated later call site that merely happened to be the first to use the broken Context.
It also prevents a broken Context from being stored, passed around, or partially used for a while before the null ever gets exercised, shrinking the blast radius and making the bug reproducible on the very first attempt to build the object rather than intermittently, depending on which code path runs first.
74. Walk through diagnosing a production incident where a Strategy implementation held mutable instance state and was incorrectly registered as a singleton, using thread dumps and load reproduction to confirm the root cause before applying the fix described earlier.
The first signal was usually a subtle, intermittent data-correctness report (wrong totals for a small percentage of requests) rather than an outright crash, which pointed away from an obvious exception and toward shared state. Capturing a thread dump under load and correlating request timestamps with the incorrect values narrowed the suspect down to a single shared bean instance being accessed by many request threads concurrently.
// Reproduce under controlled concurrency in a test to confirm the hypothesis
ExecutorService pool = Executors.newFixedThreadPool(20);
IntStream.range(0, 1000).forEach(i -> pool.submit(() -> sharedStrategy.apply(price)));
// assert the shared strategy's internal state or outputs are corrupted under concurrent access
Reproducing the corruption deterministically in a concurrency test, rather than relying only on production log correlation, confirmed the diagnosis before the team committed to the stateless refactor from Q58.
75. What smell indicates that Strategy selection logic has become scattered across many call sites instead of centralized in one factory or registry, and how would you consolidate it?
The smell is the same small if (method.equals("CREDIT_CARD")) ... else if ... mapping, or an equivalent switch statement, appearing independently in several controllers, services, or test helpers, each maintained separately and prone to drifting out of sync with one another as new strategies are added.
Consolidate by introducing a single factory or registry (see Q7, Q8) that owns the mapping exclusively, then replace every scattered copy with a call to that one shared component; a codebase-wide search for the string literals used in the branches ("CREDIT_CARD", "PAYPAL") is usually the fastest way to find every place that needs updating.
76. Describe a case where introducing the full Strategy pattern (an interface plus several classes) was overkill for a genuinely simple, permanently fixed two-way choice, and a plain if/else or a single lambda parameter would have been clearer.
A team introduced a RoundingStrategy interface with RoundUpStrategy and RoundDownStrategy classes for a currency-formatting decision that was, in practice, permanently fixed by a single, never-changing business rule and never configured or swapped anywhere in the codebase.
// Overkill for something that never actually varies at runtime
interface RoundingStrategy { java.math.BigDecimal round(java.math.BigDecimal amount); }
// Simpler and equally clear, since there is truly only ever one behavior
java.math.BigDecimal rounded = amount.setScale(2, java.math.RoundingMode.HALF_UP);
The interface and two classes added navigation overhead and test surface without ever being exercised polymorphically; a direct method call (or, if genuinely parameterized once, a single boolean flag) communicated the same fixed behavior with far less ceremony.
77. Give concrete guidance for deciding between a lambda-based inline strategy, a plain if/else statement, and a full Strategy class hierarchy for a given piece of conditional behavior.
Reach for a plain if/else when the choice is genuinely fixed, has at most a couple of branches, and is never configured, injected, or reused elsewhere. Reach for a lambda-based strategy parameter when the behavior is stateless, varies per call site, and benefits from being supplied inline without a named class. Reach for a full class-based Strategy hierarchy when implementations need state, multiple collaborating methods, dependency injection, or independent unit testing.
A useful rule of thumb: introduce the pattern only once you can name at least two genuinely different call sites (or a real, anticipated future one) that need the behavior to vary independently; adding structure for a single, unchanging case is speculative generality.
78. Describe how a Strategy implementation can leak adaptee-specific or vendor-specific types through its return value, breaking the Context's ability to treat all strategies uniformly — how would you fix this leaky abstraction?
// Leaky: return type exposes one vendor's specific response shape
interface PaymentStrategy {
StripeChargeResponse pay(java.math.BigDecimal amount); // BUG: Stripe-specific type in the shared interface
}
// Fixed: a vendor-neutral domain result every strategy can return uniformly
interface PaymentStrategy {
PaymentResult pay(java.math.BigDecimal amount); // PaymentResult is a plain domain type
}
Once one strategy's implementation detail leaks into the shared interface's signature, every other implementation is forced to either depend on that vendor's types too or fabricate a fake response shape, defeating the whole point of a common interface; the fix is a shared, vendor-neutral result type that every ConcreteStrategy maps its own internal response into.
79. Show how to implement the Strategy pattern using a Java enum, where each enum constant provides its own implementation of an abstract method (constant-specific method bodies).
enum DiscountStrategy {
BRONZE {
public java.math.BigDecimal apply(java.math.BigDecimal price) { return price; }
},
GOLD {
public java.math.BigDecimal apply(java.math.BigDecimal price) { return price.multiply(java.math.BigDecimal.valueOf(0.9)); }
},
PLATINUM {
public java.math.BigDecimal apply(java.math.BigDecimal price) { return price.multiply(java.math.BigDecimal.valueOf(0.8)); }
};
public abstract java.math.BigDecimal apply(java.math.BigDecimal price);
}
java.math.BigDecimal price = DiscountStrategy.GOLD.apply(originalPrice);
Each enum constant behaves like its own tiny ConcreteStrategy class, without needing a separate top-level class per implementation.
80. What are constant-specific method bodies in a Java enum, and how do they let an enum simultaneously serve as a fixed, closed set of ConcreteStrategy implementations?
A constant-specific method body is the { ... } block following an individual enum constant that overrides an abstract (or overridable) method declared on the enum itself, meaning each constant is actually backed by its own anonymous subclass of the enum type generated by the compiler.
Because every constant is a distinct object with its own method implementation, and the full set of constants is fixed once the enum is compiled, the enum type as a whole behaves exactly like a closed family of ConcreteStrategy implementations, iterable via values() and exhaustively checkable in a switch expression.
81. Compare an enum-based Strategy implementation to a classic interface-plus-classes hierarchy — what do you gain (exhaustive switch checking, singleton-per-constant) and what do you lose (extensibility by third parties, per-instance state)?
You gain compiler-enforced exhaustiveness in a switch expression over the enum (a missing case is a compile error), automatic, thread-safe singleton behavior per constant with no extra effort, and a naturally serializable, comparable, reflectively enumerable set of strategies via values().
You lose the ability for external code, a plugin, or configuration to add a wholly new implementation without editing and recompiling the enum's own source file, and you lose the ability to give any one implementation its own genuinely independent per-instance mutable state or constructor-injected dependencies (an enum constant can take constructor arguments, but they must be the same shape across all constants).
82. What is the key limitation of enum-based strategies that makes them unsuitable when new strategies must be added by external plugins or configuration at runtime rather than by editing the enum's source file?
An enum's set of constants is fixed at compile time inside its own source file; there is no supported mechanism for another JAR, a plugin, or a runtime configuration file to add a brand-new constant to an existing enum type. Any new variant necessarily requires editing and recompiling the enum itself.
When strategies must be contributed by third parties or toggled purely through configuration (see Q83-Q86), an interface-plus-classes design with a registry or ServiceLoader-based discovery mechanism is the only option that supports that kind of open-ended extensibility.
83. Design a Strategy registry pattern where new ConcreteStrategy implementations can be registered by name at application startup (e.g., via a ServiceLoader or a Spring bean map) without modifying the registry's own source code.
class NotificationStrategyRegistry {
private final java.util.Map<String, NotificationStrategy> strategies = new java.util.concurrent.ConcurrentHashMap<>();
void register(String channel, NotificationStrategy strategy) { strategies.put(channel, strategy); }
NotificationStrategy get(String channel) { return strategies.get(channel); }
}
// At startup, populate from wherever strategies are discovered (Spring beans, ServiceLoader, config)
registry.register("EMAIL", new EmailNotificationStrategy());
registry.register("SMS", new SmsNotificationStrategy());
The registry class itself never needs to change to support a new channel; only the startup wiring that populates it changes, keeping the registry closed for modification and open for extension.
84. Explain how a plugin architecture can use the Strategy pattern together with Java's ServiceLoader to let third-party JARs contribute new strategy implementations discovered automatically at runtime.
// In a plugin JAR: META-INF/services/com.example.NotificationStrategy contains the implementation's class name
public class SlackNotificationStrategy implements NotificationStrategy { /* ... */ }
// Host application discovers every implementation on the classpath automatically
java.util.ServiceLoader<NotificationStrategy> loader = java.util.ServiceLoader.load(NotificationStrategy.class);
for (NotificationStrategy strategy : loader) {
registry.register(strategy.channelName(), strategy);
}
ServiceLoader discovers every class listed in a plugin JAR's META-INF/services provider-configuration file and instantiates it via a no-argument constructor, letting a completely separate JAR contribute a new NotificationStrategy without the host application knowing that class exists at compile time.
85. How does keying Strategy lookup through a registry (rather than a hardcoded switch/if-chain) support the open/closed principle, and what does the registry approach cost in terms of compile-time safety?
A registry supports the open/closed principle because adding a new strategy means registering a new entry, not editing an existing switch statement's cases; the registry's own resolution code, a simple map lookup, never has to change no matter how many strategies are added over the application's lifetime.
The cost is losing the compiler's ability to verify exhaustiveness: a switch expression over a fixed set of cases fails to compile if a case is missing, while a runtime map lookup for an unregistered key only fails at runtime, typically with an exception thrown from the registry's get or resolve method (see Q8).
86. Show how a Strategy factory can read its strategy-selection key from external configuration (a properties file, environment variable, or database row) so the active strategy can change per deployment without a code change.
class ConfiguredDiscountStrategyFactory {
private final String configuredKey; // e.g. loaded from application.properties: "discount.strategy=SEASONAL"
ConfiguredDiscountStrategyFactory(java.util.Properties config) {
this.configuredKey = config.getProperty("discount.strategy", "STANDARD");
}
DiscountStrategy create() {
return switch (configuredKey) {
case "SEASONAL" -> new SeasonalDiscountStrategy();
case "STANDARD" -> new StandardDiscountStrategy();
default -> throw new IllegalStateException("Unknown discount.strategy: " + configuredKey);
};
}
}
Because the selection key comes from configuration read at startup (or, for a database row, re-read periodically), which strategy is active can change per environment or per deployment purely through configuration, with no code change or redeploy required for the switch itself.
87. Design a TaxCalculationStrategy interface with different ConcreteStrategy implementations for different tax jurisdictions (e.g., US sales tax, EU VAT), and show how a Context selects the correct strategy based on a customer's shipping address.
interface TaxCalculationStrategy {
java.math.BigDecimal calculateTax(java.math.BigDecimal subtotal);
}
class UsSalesTaxStrategy implements TaxCalculationStrategy {
private final java.math.BigDecimal stateRate;
UsSalesTaxStrategy(java.math.BigDecimal stateRate) { this.stateRate = stateRate; }
public java.math.BigDecimal calculateTax(java.math.BigDecimal subtotal) { return subtotal.multiply(stateRate); }
}
class EuVatStrategy implements TaxCalculationStrategy {
public java.math.BigDecimal calculateTax(java.math.BigDecimal subtotal) {
return subtotal.multiply(java.math.BigDecimal.valueOf(0.20)); // flat VAT rate example
}
}
class OrderTotalCalculator {
java.math.BigDecimal total(java.math.BigDecimal subtotal, TaxCalculationStrategy taxStrategy) {
return subtotal.add(taxStrategy.calculateTax(subtotal));
}
}
A factory resolves the correct TaxCalculationStrategy from the customer's shipping country and, for the US case, state, before the order calculator is invoked.
88. Design a CompressionStrategy interface (e.g., GZIP vs. Deflate vs. no-compression) selected based on payload size or client capability, and show how a Context uses it when writing an HTTP response body.
interface CompressionStrategy {
byte[] compress(byte[] data);
}
class GzipCompressionStrategy implements CompressionStrategy {
public byte[] compress(byte[] data) { /* java.util.zip.GZIPOutputStream */ return data; }
}
class NoCompressionStrategy implements CompressionStrategy {
public byte[] compress(byte[] data) { return data; }
}
class ResponseWriter {
byte[] write(byte[] body, CompressionStrategy strategy) { return strategy.compress(body); }
}
CompressionStrategy strategy = body.length > 1024 && clientAcceptsGzip
? new GzipCompressionStrategy() : new NoCompressionStrategy();
The selection logic, checking payload size and the client's Accept-Encoding header, lives in one place upstream of ResponseWriter, which itself has no idea compression is even conditional.
89. Describe a sorting scenario where the Strategy used to sort a collection is chosen based on the dataset's size or characteristics at runtime (e.g., insertion sort for small arrays, a general-purpose sort for large ones) — how would you structure this as a Strategy?
interface SortStrategy<T> {
void sort(T[] array, java.util.Comparator<T> comparator);
}
class InsertionSortStrategy<T> implements SortStrategy<T> { /* good for small, nearly-sorted arrays */ }
class DualPivotQuickSortStrategy<T> implements SortStrategy<T> { /* good for large, unordered arrays */ }
class AdaptiveSorter<T> {
void sort(T[] array, java.util.Comparator<T> comparator) {
SortStrategy<T> strategy = array.length < 32 ? new InsertionSortStrategy<>() : new DualPivotQuickSortStrategy<>();
strategy.sort(array, comparator);
}
}
This mirrors how the JDK's own Arrays.sort internally switches algorithms below a size threshold, illustrating that Strategy selection criteria need not be external configuration at all; the data itself can drive the choice.
90. Design a RetryBackoffStrategy interface with fixed-delay, exponential-backoff, and jittered-backoff ConcreteStrategy implementations, and show how a retrying HTTP client Context delegates to the injected strategy between attempts.
interface RetryBackoffStrategy {
java.time.Duration delayFor(int attemptNumber);
}
class ExponentialBackoffStrategy implements RetryBackoffStrategy {
public java.time.Duration delayFor(int attemptNumber) {
return java.time.Duration.ofMillis((long) (100 * Math.pow(2, attemptNumber)));
}
}
class RetryingHttpClient {
private final RetryBackoffStrategy backoff;
RetryingHttpClient(RetryBackoffStrategy backoff) { this.backoff = backoff; }
Response send(Request request) throws InterruptedException {
for (int attempt = 0; attempt < 5; attempt++) {
try { return doSend(request); }
catch (TransientException ex) { Thread.sleep(backoff.delayFor(attempt).toMillis()); }
}
throw new RetriesExhaustedException(request);
}
}
91. Show how a chain of ValidationStrategy implementations can be applied to an incoming request, each responsible for one validation rule, and explain how this differs from the Chain of Responsibility pattern even though both apply a sequence of checks.
interface ValidationStrategy {
java.util.Optional<String> validate(Order order);
}
class RequestValidator {
private final java.util.List<ValidationStrategy> rules;
RequestValidator(java.util.List<ValidationStrategy> rules) { this.rules = rules; }
java.util.List<String> validate(Order order) {
return rules.stream().flatMap(r -> r.validate(order).stream()).toList();
}
}
Here every rule always runs and the Context collects every failure, which is a Strategy-flavored composite (see Q64, Q68): the Context owns the fixed iteration and aggregation logic, and each rule is a fully interchangeable, independent unit. True Chain of Responsibility instead gives each handler the power to decide whether to pass the request further down the chain at all, and typically stops at the first handler that fully handles it, which is a materially different control-flow contract.
92. Design a NotificationChannelStrategy interface with email, SMS, and push ConcreteStrategy implementations, and show how a Context picks the right one(s) based on a user's notification preferences.
interface NotificationChannelStrategy {
void send(User user, String message);
}
class EmailChannelStrategy implements NotificationChannelStrategy { public void send(User user, String message) { /* SMTP */ } }
class SmsChannelStrategy implements NotificationChannelStrategy { public void send(User user, String message) { /* SMS gateway */ } }
class NotificationDispatcher {
void notify(User user, String message, java.util.List<NotificationChannelStrategy> channels) {
channels.forEach(channel -> channel.send(user, message));
}
}
java.util.List<NotificationChannelStrategy> enabled = user.preferredChannels().stream()
.map(channelRegistry::resolve)
.toList();
dispatcher.notify(user, "Your order shipped", enabled);
The user's stored preferences drive which strategies get resolved and passed in; NotificationDispatcher itself has no notion of email, SMS, or push at all.
93. Walk through designing a routing or pricing Strategy interface for a ride-sharing or logistics application, where the ConcreteStrategy chosen depends on real-time conditions such as demand, distance, or time of day.
interface FarePricingStrategy {
java.math.BigDecimal calculateFare(RideRequest request);
}
class SurgePricingStrategy implements FarePricingStrategy {
private final java.math.BigDecimal surgeMultiplier;
public java.math.BigDecimal calculateFare(RideRequest request) {
return baseFare(request).multiply(surgeMultiplier);
}
private java.math.BigDecimal baseFare(RideRequest request) { return java.math.BigDecimal.valueOf(request.distanceKm() * 1.2); }
}
class FarePricingSelector {
FarePricingStrategy select(DemandSnapshot demand) {
return demand.isHighDemand() ? new SurgePricingStrategy(demand.multiplier()) : new StandardFareStrategy();
}
}
Real-time signals (current demand, time of day) feed a selector that resolves the appropriate strategy per ride request, so the fare calculator itself never needs to know why one multiplier or another applies.
94. What should Javadoc on a Strategy interface and each ConcreteStrategy implementation document to help future maintainers understand when to add a new implementation versus modifying an existing one?
The Strategy interface's Javadoc should state the contract precisely (what each parameter means, what the return value represents, any thrown exceptions callers must handle) and note that new variants belong as new implementations, not as added parameters or branches inside an existing one.
Each ConcreteStrategy's Javadoc should state which specific business rule, vendor, or condition it represents and why it exists as a separate class rather than a variant of another implementation, so a maintainer tempted to add a flag to an existing strategy instead recognizes that a new implementation is the correct move.
95. Describe a step-by-step approach for incrementally migrating a legacy codebase's large if/else or switch-based conditional logic into a proper Strategy pattern without a risky big-bang rewrite.
First, extract the entire conditional block, unchanged, into a single private method so its boundaries are explicit and it is covered by characterization tests before anything else moves. Next, extract just one branch's body into its own ConcreteStrategy class implementing a newly introduced interface, leaving every other branch untouched, and verify tests still pass.
// Step 2: only the first branch becomes a real strategy; the rest still falls through the old chain
DiscountStrategy strategy = tier == CustomerTier.GOLD
? new GoldDiscountStrategy()
: legacyApplyDiscount(tier, price); // old logic, still intact for now
Repeat branch by branch until the original conditional method contains nothing but a factory lookup, then delete it in favor of the registry or factory built along the way (see Q7, Q83), keeping every intermediate step independently shippable and revertible.
96. Explain how the Strategy pattern can be combined with the Decorator pattern, for example wrapping a chosen pricing Strategy with a logging or caching decorator that implements the same Strategy interface.
class LoggingDiscountStrategy implements DiscountStrategy {
private final DiscountStrategy delegate;
LoggingDiscountStrategy(DiscountStrategy delegate) { this.delegate = delegate; }
public java.math.BigDecimal apply(java.math.BigDecimal price) {
java.math.BigDecimal result = delegate.apply(price);
log.info("Applied {} to {} -> {}", delegate.getClass().getSimpleName(), price, result);
return result;
}
}
DiscountStrategy strategy = new LoggingDiscountStrategy(new GoldDiscountStrategy());
The decorator implements the same DiscountStrategy interface as the strategy it wraps, adding logging, caching, or timing around the call without the Context ever needing to know whether it holds a raw strategy or a decorated one — the two patterns compose cleanly because Decorator preserves the interface Strategy already established.
97. Describe how you would write an end-to-end test verifying that a Context correctly resolves and applies the right ConcreteStrategy given a specific configuration or request input, from factory lookup through to the final computed result.
@Test
void goldTierCustomerReceivesTenPercentOff() {
PricingEngine engine = new PricingEngine(realTierStrategyMap()); // real strategies, not mocks
Customer goldCustomer = new Customer(CustomerTier.GOLD);
java.math.BigDecimal price = engine.priceFor(goldCustomer, java.math.BigDecimal.valueOf(100));
assertThat(price).isEqualByComparingTo("90.00");
}
An end-to-end test wires the real factory or registry, the real strategy implementations, and the real Context together, then asserts on the observable final result for a representative set of inputs, catching wiring mistakes (a missing registration, a wrong qualifier) that a pure unit test with mocks would never expose.
98. Why should a Strategy interface stay narrow and cohesive (interface segregation) rather than growing extra methods over time to serve one implementation's special needs, and what problem does a bloated Strategy interface cause for other implementations?
Every method added to a shared Strategy interface must be implemented by every existing ConcreteStrategy, even ones for which that method is meaningless; a bloated interface forces unrelated implementations to supply awkward no-op or exception-throwing bodies purely to satisfy a method they never needed.
// Bad: interface grew a method that only one implementation actually needs
interface PaymentStrategy {
void pay(java.math.BigDecimal amount);
void payWithInstallments(java.math.BigDecimal amount, int installments); // only CreditCardStrategy supports this
}
If only one implementation genuinely needs an extra capability, that capability belongs on a separate, narrower interface that only the strategies supporting it implement, per the interface segregation principle, rather than forcing every strategy to grow alongside it.
99. How would you version a Strategy interface's contract (e.g., adding a new method) without breaking existing ConcreteStrategy implementations that only implement the original contract, using default methods?
interface PaymentStrategy {
PaymentResult pay(java.math.BigDecimal amount);
// Added later: a default implementation keeps every existing ConcreteStrategy compiling unchanged
default PaymentResult payWithMetadata(java.math.BigDecimal amount, java.util.Map<String, String> metadata) {
return pay(amount); // sensible fallback ignoring metadata, until an implementation overrides it
}
}
A default method lets you extend an interface's contract without forcing a recompile-and-break of every implementer; existing strategies simply inherit the default behavior, and only implementations that genuinely need the new capability override it explicitly.
100. In a system design or coding interview, how would you decide whether a piece of interchangeable, runtime-selectable behavior calls for the full Strategy pattern versus a simpler alternative such as a single lambda parameter or a plain conditional?
The deciding factors are how many genuinely distinct implementations exist or are expected, whether they carry state or dependencies beyond a single expression, and whether the choice must be swappable at runtime rather than fixed once at compile time. A single, stateless, one-line behavior injected at one call site is a lambda parameter; several stateful, independently testable, dependency-injected implementations resolved through configuration is the full class-based Strategy pattern with a factory or registry.
A strong interview answer names this trade-off explicitly and mentions the pattern's near-neighbors by contrast: "if the behavior naturally changes as a side effect of the object's own state, that is State, not Strategy; if only one fixed step of an otherwise-fixed algorithm varies, that is Template Method; if this needs undo or queuing, that is Command" — showing the reasoning that distinguishes an applied pattern from a recited one.
Post a Comment
Add