ArchitectMind
  1. Home
  2. Tutorials
  3. Book 5 · Software Design & Design Patterns
  4. Volume IV
  5. Strategy vs Conditional Logic
Book 5 · Volume IV — Behavioral Design Patterns · Topic 1.3

Strategy vs Conditional Logic

The if/else explosion and extension without modification

9 min readStage 2 · Frameworks & design8 interview questions
Chapter
Chapter 1 — Strategy
Prerequisites
Topics 1.1–1.2; Volume II Topic 3.x (Open/Closed)
You will be able to
Recognise when conditional logic has become an explosion, refactor it safely into strategies, extend behaviour without modifying existing code, and decide when a switch expression over a sealed type is the better modern alternative
Covers
if/else explosion; Extension
On this page
  1. 1. Concept / Definition
  2. 2. Why It Exists
  3. 3. Core Concepts
  4. 4. How It Works Internally — The Refactoring Path
  5. 4A. Deep Dive — The Modern Alternative: switch Over Sealed Types
  6. 5. APIs / Features / Syntax
  7. 6. Practical Examples
  8. 7. Common Problems & Mistakes
  9. 8. Performance & Production Considerations
  10. 9. Architecture & Design Considerations
  11. 10. Interview Questions & Answers

1Concept / Definition

Conditional logic (if/else, switch) and the Strategy pattern are two ways to express variation. Conditionals put all variants in one place and choose among them by testing; Strategy puts each variant in its own object and chooses by selecting it once.

The if/else explosion
Figure 1 — The if/else explosion
AspectConditional logicStrategy
Where variants liveOne method (often several)One class/lambda each
Adding a variantModify existing method(s)Add a new strategy
Choosing the variantAt every decision pointOnce, in a selector
Readability at 2–3 variantsExcellentSlightly heavier
Readability at 10+ variantsPoorStays constant per strategy
Table 1 — Two ways to express variation

2Why It Exists

Most code starts with an if. That is correct: introducing a pattern for two stable cases is over-engineering. But certain variation axes keep growing — countries, payment methods, customer segments, file formats — and each addition edits the same methods. Merge conflicts, regression risk and cognitive load rise with every branch. Recognising the moment to switch from conditionals to strategies, and doing the refactoring safely, is a core design skill.

3Core Concepts

3.1 if/else explosion

An if/else explosion has recognisable symptoms:

SymptomExample
The same condition in several methodsswitch (country) in shippingCost(), eta(), labelFormat(), customsForm()
Nested conditions across two axesif (country == IN) { if (express) ... }
Branches growing every releaseEach new market adds branches in 6 files
Branches owned by different teamsTax team and logistics team edit the same method
Tests per method explodeEvery branch combination needs coverage
Flags accumulatingboolean newPricing, useV2Rounding, isPilotTenant
Table 2 — Explosion symptoms
JAVA
// A typical explosion (simplified)
public Money shippingCost(Order o) {
    if (o.country() == Country.IN) {
        if (o.express()) return Money.inr(149);
        return o.total().isAtLeast(Money.inr(499)) ? Money.ZERO : Money.inr(49);
    } else if (o.country() == Country.DE) {
        if (o.bulky()) return Money.eur("29.00");
        return o.total().isAtLeast(Money.eur("50")) ? Money.ZERO : Money.eur("4.90");
    } else if (o.country() == Country.US) {
        return o.express() ? Money.usd("19.99") : Money.usd("6.99");
    }
    throw new UnsupportedOperationException("country " + o.country());
}
// ...and a parallel switch in eta(Order), carrier(Order), label(Order)
Complexity growth — one method vs many small ones
Figure 2 — Complexity growth — one method vs many small ones

The cyclomatic complexity of the method grows linearly with each country; with strategies, complexity per unit stays constant and the selection is a single map lookup.

3.2 Extension

The Open/Closed Principle asks that adding behaviour should not require modifying tested code. With Strategy, extension means adding a class and registering it:

JAVA
public interface CountryShipping {
    Country country();
    Money cost(Order o);
    Duration eta(Order o);
    Carrier carrier(Order o);
}
 
@Component final class IndiaShipping implements CountryShipping {
    public Country country() { return Country.IN; }
    public Money cost(Order o) {
        if (o.express()) return Money.inr(149);
        return o.total().isAtLeast(Money.inr(499)) ? Money.ZERO : Money.inr(49);
    }
    public Duration eta(Order o) {
        return o.express() ? Duration.ofDays(1) : Duration.ofDays(4);
    }
    public Carrier carrier(Order o) { return Carrier.DELHIVERY; }
}
 
@Component
final class ShippingRules {
    private final Map<Country, CountryShipping> byCountry;
    ShippingRules(List<CountryShipping> all) {
        byCountry = all.stream().collect(
            Collectors.toUnmodifiableMap(CountryShipping::country, s -> s));
    }
    CountryShipping forCountry(Country c) {
        var s = byCountry.get(c);
        if (s == null) throw new UnsupportedMarket(c);
        return s;
    }
}

Adding Japan is now a new JapanShipping class — no existing class changes, no existing tests need rerunning for correctness, and ownership can be delegated to the team launching the market.

ChangeConditional designStrategy design
Add JapanEdit 4 methods, add 4+ branchesAdd 1 class
Change India express priceEdit a branch among othersEdit IndiaShipping only
Test India rulesThrough a big methodIndiaShipping unit tests
Who reviewsEveryone touching shippingMarket team
Table 3 — Extension comparison

4How It Works Internally — The Refactoring Path

Refactoring conditionals to Strategy
Figure 3 — Refactoring conditionals to Strategy

1. Pin behaviour with characterisation tests covering each branch (golden inputs → outputs).

2. Find the axis the conditional switches on (country, type, segment).

3. Extract the interface with one method per decision that switches on that axis.

4. Move one branch at a time into a strategy class; route that key through the new path while others still use the old code.

5. Build the registry keyed by the axis; add a default or explicit failure for unknown keys.

6. Delete the conditional when all branches have moved; tests stay green throughout.

This incremental approach — Replace Conditional with Polymorphism — keeps the system releasable at every step.

4ADeep Dive — The Modern Alternative: switch Over Sealed Types

Java 21+ pattern matching changes the trade-off. When the variants are a closed set of data types and the operations are owned by one team, an exhaustive switch can be clearer than strategies:

JAVA
public sealed interface PaymentMethod permits Card, Upi, BankTransfer {}
public record Card(String token, String network) implements PaymentMethod {}
public record Upi(String vpa) implements PaymentMethod {}
public record BankTransfer(String iban) implements PaymentMethod {}
 
static Money fee(PaymentMethod m, Money amount) {
    return switch (m) {                         // exhaustive: no default needed
        case Card c when c.network().equals("AMEX") -> amount.percent("2.9");
        case Card c         -> amount.percent("1.8");
        case Upi u          -> Money.ZERO;
        case BankTransfer b -> Money.eur("0.35");
    };
}
Keep the if/else or introduce Strategy?
Figure 4 — Keep the if/else or introduce Strategy?
CriterionSealed type + switchStrategy
Variant setClosed, rarely changesOpen, grows over time
Who adds variantsSame team as the operationsOther teams / plug-ins / config
Compiler supportExhaustiveness check on every switchNone needed — polymorphism
Adding an operationOne new functionNew method in every strategy
Adding a variantCompiler flags every switch to updateOne new class
Runtime selection by configAwkwardNatural
Table 4 — Choosing the modern way

This is the classic expression problem: switches make new operations cheap; polymorphism makes new variants cheap. Choose based on which will change more.

5APIs / Features / Syntax

FeatureRole
Switch expressions (->, yield)Concise, expression-valued conditionals
Pattern matching with guards (when)Conditions on data shape
Sealed interfacesExhaustiveness checking
Map<K, Strategy> / EnumMapStrategy registries
Spring List<T> injectionAutomatic registration
ArchUnit / PMD cyclomatic-complexity rulesDetect explosions early
Table 5 — Tools for managing variation

EnumMap is an efficient registry when the key is an enum:

JAVA
Map<Country, CountryShipping> byCountry = new EnumMap<>(Country.class);

6Practical Examples

6.1 Characterisation test before refactoring

JAVA
@ParameterizedTest
@CsvSource({ "IN,false,300,49.00", "IN,false,600,0.00", "IN,true,600,149.00",
             "DE,false,40,4.90",  "DE,true,40,4.90",  "US,true,10,19.99" })
void shippingCostUnchanged(Country c, boolean express, int total, String expected) {
    assertThat(shipping.cost(order(c, express, total))).isEqualTo(money(c, expected));
}

The same test runs before, during and after the refactoring.

6.2 Removing a boolean flag

calculate(order, useNewPricing) becomes calculate(order, pricingStrategy); the flag's two branches become two strategies selected by a feature-flag lookup in one place — and the old one is deleted after rollout.

6.3 When the conditional stayed

A switch over three HTTP status families (2xx/4xx/5xx) in one error mapper was left as is: stable, local and readable. Introducing strategies would have added files without reducing risk.

6.4 Worked refactoring — a payment-method fee calculator

Starting point. A FeeService with four methods (fee, settlementDays, refundable, displayName) each containing a switch over a String method code ("CARD", "UPI", "NETBANKING", "WALLET"). Two new methods (BNPL, EMI) are planned.

StepChangeTests
1Parameterised characterisation tests for all four methods × four codes16 cases green
2Introduce PaymentMethodRules interface with the four operationsunchanged
3Move CARD branches into CardRules; FeeService delegates for CARD onlygreen
4Repeat for UPI, NETBANKING, WALLETgreen after each
5Registry Map<String, PaymentMethodRules> built from beans; unknown code → UnsupportedMethod+1 negative test
6Delete the four switchesgreen
7Add BnplRules, EmiRules as new classesnew tests only
Table 6 — Refactoring steps
JAVA
public interface PaymentMethodRules {
    String code();
    Money fee(Money amount);
    int settlementDays();
    boolean refundable();
    String displayName(Locale locale);
}
 
@Component
final class UpiRules implements PaymentMethodRules {
    public String code() { return "UPI"; }
    public Money fee(Money amount) { return Money.ZERO; }
    public int settlementDays() { return 1; }
    public boolean refundable() { return true; }
    public String displayName(Locale l) { return "UPI"; }
}
 
@Service
final class FeeService {
    private final Map<String, PaymentMethodRules> rules;
    FeeService(List<PaymentMethodRules> all) {
        rules = all.stream().collect(
            Collectors.toUnmodifiableMap(PaymentMethodRules::code, r -> r));
    }
    Money fee(String code, Money amount) { return rulesFor(code).fee(amount); }
    private PaymentMethodRules rulesFor(String code) {
        var r = rules.get(code);
        if (r == null) throw new UnsupportedMethod(code);
        return r;
    }
}
MetricBeforeAfter
Largest method complexity143
Files touched to add BNPL (estimate)1 file, 4 methods, 4 switches1 new class
Merge conflicts per quarter (team estimate)frequentrare
Table 7 — Outcome

7Common Problems & Mistakes

  • Premature Strategy for two stable branches.
  • Big-bang refactoring without characterisation tests.
  • Moving the switch into a factory that is then duplicated elsewhere.
  • Strategies that still switch on the same axis internally.
  • Missing default for unknown keys → NullPointerException at runtime.
  • Ignoring the expression problem — using strategies where operations, not variants, keep growing.

8Performance & Production Considerations

AspectConditionalStrategy
Execution speedBranch prediction; very fastMap lookup + virtual call; very fast
Deployment risk of a changeWhole method redeployed/retestedOnly the affected strategy
ObservabilityBranch taken is implicitStrategy name loggable/metric-taggable
Config-driven changesCode changeRegistry/config change possible
Table 8 — Operational comparison

Performance rarely decides between them; maintainability and change ownership do.

9Architecture & Design Considerations

  • Track cyclomatic complexity and change frequency together: complex and frequently changed methods are prime Strategy candidates.
  • Align strategy boundaries with team ownership (market, product line, tenant).
  • Prefer sealed types + switch for closed domain variants; strategies for open, configurable or externally extended variants.
  • Record the choice in an ADR — it predicts where future changes will land.

10Interview Questions & Answers

Q1BasicConcept

What is an if/else explosion?

An if/else explosion is conditional logic that keeps growing as new variants of the same concept are added, typically countries, payment methods, customer segments or file formats. The giveaway is not a single long if chain but repetition and growth: the same switch on country appears in shippingCost, eta, carrier and label methods, conditions nest across two axes such as country and express delivery, and every release adds branches in several files, with feature flags accumulating on top. The effects are rising cyclomatic complexity, merge conflicts between teams editing the same methods, an exploding number of test combinations and a high risk that a new branch breaks an existing one.

Q2BasicConcept

How does Strategy support extension without modification?

Strategy turns each variant into its own class or lambda behind a common interface, and a selector chooses the right one once, typically from a map keyed by the variation axis. Adding a variant then means writing a new class and registering it; existing strategies, the selector and their tests stay untouched, which is the Open/Closed Principle in practice. In Spring the registration is automatic: inject List<CountryShipping>, build an unmodifiable map by country, and a new @Component JapanShipping is picked up on the next start. Ownership becomes clearer too, because the team launching a market owns its strategy class and tests without editing anyone else's branches.

Q3IntermediateTrade-off

When should you keep a conditional instead of introducing Strategy?

Keep the conditional when the variation is small, stable and local. Two or three branches that rarely change, live in one method and are owned by one team are clearer as an if or a switch expression than as an interface plus several classes and a registry. Examples are mapping HTTP status families in an error handler or choosing between a couple of rounding modes. Introducing Strategy there adds files and indirection without reducing risk, which is premature abstraction. Switch to Strategy when the same condition starts appearing in several methods, branches grow every release, variants need runtime selection by configuration, or different teams need to own different variants.

Q4IntermediateCode

What are the steps to refactor a conditional into strategies?

First, pin current behaviour with characterisation tests covering every branch, often a parameterised test with golden inputs and outputs. Second, identify the axis the conditionals switch on, such as country or payment code. Third, extract an interface with one method per decision that switches on that axis. Fourth, move one branch at a time into a strategy class and route only that key through the new path while the others still use the old code, running the tests after each move. Fifth, build a registry keyed by the axis, with an explicit failure for unknown keys. Finally, delete the old conditional once every branch has moved. The system stays releasable throughout.

Q5Senior EngineerTrade-off

How do sealed types and pattern-matching switch change the decision?

They give conditionals compile-time safety, so the choice is no longer simply polymorphism good, switch bad. With a sealed interface whose permitted subtypes are records, a pattern-matching switch is exhaustive: no default branch is needed, and adding a new subtype makes the compiler flag every switch that must handle it. Guards with when handle finer conditions such as the card network. This is ideal when the variant set is closed, data-like and owned by the same team as the operations, and new operations arrive more often than new variants. Strategy remains better when variants are open, selected by configuration at runtime, contributed by other teams or plug-ins, or carry their own dependencies.

Q6Senior EngineerConcept

What is the expression problem in this context?

The expression problem says no single structure makes it cheap to add both new variants and new operations. With switches over a sealed type, operations are functions outside the types, so adding an operation such as refundPolicy is one new function, but adding a variant forces an edit to every switch. With Strategy or polymorphism, each variant is a class implementing all operations, so adding a variant is one new class, but adding an operation means changing the interface and every implementation. The practical rule is to look at change history and predict which axis will grow: choose Strategy when variants multiply, and sealed types with switches when operations do.

Q7Technical ArchitectScenario

How would you detect candidates for this refactoring in a large codebase?

I combine complexity with change frequency, because a complex switch that never changes is not worth touching. SonarQube or PMD give cyclomatic and cognitive complexity per method, and Git history shows how often each file changes; the intersection, complex methods modified every few weeks, is the hotspot list. Then I look for repeated switches on the same axis using IntelliJ structural search or simple searches for switches on country or method codes, and for change coupling, where several files with parallel switches are always committed together. Merge-conflict frequency and accumulating boolean flags are further signals. The result is a short list ranked by expected payoff, not a big-bang refactoring plan.

Q8Solution ArchitectDesign

How does replacing conditionals with strategies affect team organisation?

It moves the code structure closer to team ownership, which is Conway's law working in your favour. While country or tenant rules live in shared switch statements, every team edits the same methods, so changes need cross-team review, merge conflicts are common and releases need coordination. Once each market, product line or tenant has its own strategy class, or even its own module contributing strategies through a shared interface, the owning team can change, test and deploy its rules without touching anyone else's. The shared interface becomes a contract that needs governance and careful evolution. The architectural gain is lower coordination cost and faster routine changes, more than reduced code complexity.

Newsletter

Get new tutorials every week

New ArchitectMind tutorials and interview questions, once a week. No spam, unsubscribe anytime.

Almost done! Check your inbox and click the confirmation link to complete your subscription.
Keep going in Book 5

Software Design & Design Patterns

Volume I · Topic 1.1

Software Design

Software Design Foundations & Principles

Volume I · Topic 1.2

Why Software Design Matters

Software Design Foundations & Principles

Volume I · Topic 1.3

Good vs Poor Design

Software Design Foundations & Principles

Volume II · Topic 1.1

Objects

Object-Oriented Design & SOLID

Volume II · Topic 1.2

Classes

Object-Oriented Design & SOLID

Volume II · Topic 1.3

Relationships

Object-Oriented Design & SOLID

Volume III · Topic 1.1

What Is a Design Pattern?

Creational & Structural Design Patterns

Volume III · Topic 1.2

Pattern Categories

Creational & Structural Design Patterns

Volume III · Topic 1.3

Pattern vs Framework

Creational & Structural Design Patterns

Volume IV · Topic 1.1

Strategy Pattern

Behavioral Design Patterns

Volume IV · Topic 1.2

Strategy in Java

Behavioral Design Patterns

Volume V · Topic 1.1

Layered Architecture

Enterprise & Application Design Patterns

Volume V · Topic 1.2

Service Layer

Enterprise & Application Design Patterns

Volume V · Topic 1.3

Repository

Enterprise & Application Design Patterns

Volume VI · Topic 1.1

What Is Refactoring?

Refactoring, Code Quality & Evolutionary Design

Volume VI · Topic 1.2

Why Refactor?

Refactoring, Code Quality & Evolutionary Design

Volume VI · Topic 1.3

Refactoring Safety

Refactoring, Code Quality & Evolutionary Design

Volume VII · Topic 1.1

Method-Level Design

Advanced Design & Architecture-Level Design Decisions

Volume VII · Topic 1.2

Class-Level Design

Advanced Design & Architecture-Level Design Decisions

Volume VII · Topic 1.3

Module-Level Design

Advanced Design & Architecture-Level Design Decisions