Lesson 18 of 38 12 minDesign Track

LLD Mastery: The Strategy Design Pattern

Master the Strategy pattern for Low-Level Design. Learn how to eliminate massive if-else blocks by decoupling algorithms from their context.

Reading Mode

Hide the curriculum rail and keep the lesson centered for focused reading.

Key Takeaways

  • The Strategy pattern defines a family of interchangeable algorithms, enabling the runtime selection of execution paths without hardcoded conditionals.
  • Dynamic strategy dispatching can be implemented cleanly in Java by registering strategy spring beans directly into a Map container.
  • To scale Strategy architectures under high load, lookups must be optimized via caching and by using immutable Flyweight instances.
Recommended Prerequisites
SOLID Principles in Java

Premium outcome

Bridge the gap between architecture diagrams and implementation details.

Engineers preparing for LLD rounds or leveling up their software design depth.

What you unlock

  • Cleaner reasoning around SOLID, patterns, responsibilities, and schema design
  • A usable bridge between HLD whiteboard thinking and concrete Java classes
  • Case-study practice across common interview-style design systems

Conditional statements (such as if-else or switch-case blocks) are the natural building blocks of branching logic. However, as business rules expand, embedding these conditionals within core service classes creates monolithic, fragile code that violates the Open/Closed Principle (OCP). Adding a new payment gateway, shipping provider, or tax calculation model requires modifying the same file, increasing regression risks.

The Strategy Pattern is a behavioral design pattern that defines a family of algorithms, encapsulates each one into a separate class, and makes them interchangeable at runtime. This allows the algorithm to vary independently of the clients that use it, replacing static branching with clean object composition.


System Requirements

To design an enterprise-grade dynamic algorithm execution engine (such as a multi-carrier shipping calculator or dynamic payment router), we define the following parameters:

Functional Requirements

  • Runtime Interchangeability: The system must dynamically select and execute an algorithm at runtime based on context properties (e.g., origin location, merchant tiers, payment methods).
  • Extensible Registry: Adding a new algorithm strategy must be possible by writing a new strategy class, with zero changes to the context class or calling code.
  • Dynamic Configuration Mapping: Support mapping specific user types or merchants to specific strategies via a database configuration table.

Non-Functional Requirements

  • Low Overhead Dispatch: The lookup and execution routing overhead of a strategy must be less than 1 millisecond, ensuring zero performance penalties.
  • High Throughput Thread Safety: The routing engine and strategy instances must be stateless, allowing concurrent access by thousands of worker threads.
  • Graceful Failure Fallbacks: If a selected third-party strategy fails or is unavailable, the context must automatically fall back to a default strategy.

API Design and Interface Contracts

To allow runtime strategy registration and query executions, we design clear contracts.

1. Strategy Execution Request Contract (HTTP POST /v1/shipping/calculate-rate)

Used by client applications to execute a shipping rate calculation using dynamic routing strategies.

{
  "transactionId": "txn_809124_ship",
  "merchantId": "merch_tier_1_002",
  "packageDetails": {
    "weightKg": 12.5,
    "dimensionsCm": {
      "length": 30,
      "width": 20,
      "height": 15
    }
  },
  "route": {
    "originPostalCode": "90210",
    "destinationPostalCode": "10001",
    "countryCode": "USA"
  },
  "preferredCarrier": "DYNAMIC_OPTIMIZED"
}

2. Strategy Registration Event Schema (gRPC Protocol)

Enables registering new strategy providers dynamically with a central routing engine.

syntax = "proto3";

package codesprintpro.strategy.router.v1;

service StrategyRouterService {
  rpc RegisterStrategy (RegisterStrategyRequest) returns (RegisterStrategyResponse);
}

message RegisterStrategyRequest {
  string strategy_key = 1; // E.g., 'FEDEX_PRIORITY', 'DHL_EXPRESS'
  string friendly_name = 2;
  string target_grpc_endpoint = 3;
  int32 priority_weight = 4;
  int64 registered_at_ms = 5;
}

message RegisterStrategyResponse {
  bool is_registered = 1;
  int64 active_routing_epoch = 2;
  string error_message = 3;
}

High-Level Architecture

The architecture visualizes standard client-context-strategy relationships and a dynamic dynamic payment strategy dispatching pipeline.

1. Strategy Pattern UML Runtime Flow

The Client sets the desired concrete strategy on the Context class. The Context delegates execution to the abstract Strategy interface, avoiding hardcoded dependencies.

graph TD
    Client[Client Code] -->|1. Instantiate & Set Strategy| Context[PaymentContext]
    Context -->|2. Delegate execute| Strategy[<<interface>> PaymentStrategy]
    
    Strategy1[CreditCardStrategy] -.->|Implements| Strategy
    Strategy2[PayPalStrategy] -.->|Implements| Strategy
    Strategy3[CryptoStrategy] -.->|Implements| Strategy

2. Dynamic Router Payment Gateway Pipeline

When a request is received, the Routing Context queries the Database Configuration for the active merchant rules, loads the mapped Strategy class from the Spring Application Context, and executes the gateway transfer.

sequenceDiagram
    autonumber
    participant Client as Client application
    participant Context as PaymentContext (Spring Service)
    participant DB as Postgres Strategy Config Table
    participant Registry as Spring Bean Map
    participant Gateway as External Payment API

    Client->>Context: Process Payment (Merchant A, $500)
    Context->>DB: Query active strategy rule for Merchant A
    DB-->>Context: Return strategy key "STRATEGY_ADYEN"
    Context->>Registry: Retrieve Bean for "STRATEGY_ADYEN"
    Registry-->>Context: Return AdyenPaymentStrategy instance
    Context->>Gateway: Execute adyen.pay(amount = $500)
    Gateway-->>Context: Return HTTP 200 OK (Success)
    Context-->>Client: Transaction Success Response

Low-Level Design and Schema

To support routing client requests dynamically to the correct strategy using database configurations, we declare tables in PostgreSQL.

-- Core table mapping merchant accounts to their active algorithms
CREATE TABLE merchant_routing_strategies (
    merchant_id VARCHAR(64) PRIMARY KEY,
    active_strategy_key VARCHAR(64) NOT NULL, -- E.g., 'STRATEGY_STRIPE', 'STRATEGY_BRAINTREE'
    fallback_strategy_key VARCHAR(64) NOT NULL DEFAULT 'STRATEGY_STRIPE',
    is_routing_enabled BOOLEAN NOT NULL DEFAULT TRUE,
    max_transaction_limit_usd DECIMAL(12, 2) NOT NULL,
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_merchant_routing ON merchant_routing_strategies (merchant_id) WHERE is_routing_enabled = TRUE;

-- Historic rules for auditing routing changes and configuration alterations
CREATE TABLE routing_rules_audit_log (
    audit_id BIGSERIAL PRIMARY KEY,
    merchant_id VARCHAR(64) NOT NULL,
    changed_by_user VARCHAR(128) NOT NULL,
    old_strategy_key VARCHAR(64) NOT NULL,
    new_strategy_key VARCHAR(64) NOT NULL,
    change_reason VARCHAR(256) NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_audit_lookup ON routing_rules_audit_log (merchant_id, created_at DESC);

Schema Rationale & Index Optimization:

  1. Partial Index (idx_merchant_routing): Restricts the index boundary to active routing records. Under high transaction rates, the router performs index-only scans on this small table to fetch strategy keys in less than 1 millisecond.
  2. Audit Tracking (routing_rules_audit_log): Maintains historical traces for compliance, allowing teams to audit why a merchant's payment routing strategy was migrated from Stripe to Adyen during high-volume spikes.

Scaling Challenges and Capacity Estimation

Dynamic strategy execution systems must manage object creation rates and CPU lookup overhead.

1. Transient Object Allocation Footprint

Instantiating new strategy instances for every transaction in high-frequency payment loops generates massive garbage collection pressure.

  • Assumptions:

    • Request volume = $50,000$ transactions/second
    • Heap size allocated per transient strategy instance = $5$ KB
    • System operates 24/7
  • Calculations: $$\text{Heap Memory Allocation Rate} = 50,000\text{ tx/s} \times 5\text{ KB} = 250,000\text{ KB/second} \approx 244\text{ MB/second}$$ $$\text{Allocated per Hour} = 244\text{ MB/s} \times 3600 \approx 878\text{ GB/hour}$$

Allocating nearly 1 TB of heap objects per hour triggers frequent JVM Garbage Collection pauses (Stop-the-World cycles), increasing latency. To scale, the context must use Stateless Singletons or the Flyweight Pattern for strategy instances. Strategy beans should be created once during startup, registered in a thread-safe map, and reused across all requests.

2. CPU Cycles and Dispatch Performance

Dynamic string lookup vs. direct array indexing impacts CPU cycle consumption at scale.

  • Assumptions:

    • CPU frequency = $3.2$ GHz
    • Average string comparison overhead in Map lookup = $120$ clock cycles
    • Direct array/enum index lookup = $5$ clock cycles
  • Calculations: Using string keys for lookup: $$\text{CPU cycles spent per second} = 50,000\text{ tx/s} \times 120\text{ cycles} = 6,000,000\text{ cycles} \approx 6\text{ MHz}$$

    Using array index keys (Enum Strategy): $$\text{CPU cycles spent per second} = 50,000\text{ tx/s} \times 5\text{ cycles} = 250,000\text{ cycles} \approx 0.25\text{ MHz}$$

While 6 MHz is a small fraction of a modern CPU, reducing string parsing to array lookups minimizes CPU cache misses (L1 data cache invalidations), leaving more cycles available for serialization and network handling.


Failure Scenarios and Resilience

Pluggable dynamic strategy routing requires robust fallback mechanisms to handle missing keys and third-party failures.

1. Missing Strategy Configuration (Null Strategy Error)

  • The Threat: A merchant is registered in the system, but their active_strategy_key in the database does not map to any loaded class or spring bean, causing a null pointer exception.
  • Resilience Design:
    • Implement a Fallback Strategy Guard.
    • In the context class, catch any missing key lookup and assign a default provider (e.g. DefaultStripeStrategy). Record a high-priority telemetry alert for monitoring dashboards.

2. Third-Party Gateway Timeout

  • The Threat: The active strategy delegates call execution to a third-party REST API (e.g., FedEx rates). If FedEx is slow or offline, the worker threads pool becomes exhausted waiting for socket read responses.
  • Resilience Design:
    • Wrap the strategy execution in a Circuit Breaker (using Resilience4j or Sentinel) with a tight 500ms timeout threshold.
    • If the active strategy times out or the circuit trips, the context intercepts the error and routes the query to the fallback local cache strategy, returning a default pricing matrix.

Architectural Trade-offs

Selecting the correct design pattern for branching logic depends on runtime flexibility requirements.

Trade-off 1: Strategy Pattern vs. Template Method Pattern

Pattern Type Binding Time Composition Style Complexity Best Use Case
Strategy Pattern Runtime (Dynamic). Composition. Swap algorithms on the fly. Medium. Requires separate strategy classes. Dynamic third-party routing (e.g., payment gateways, shipping calculators).
Template Method Compile-time (Static). Inheritance. Subclasses override steps. Low. Simple abstract base class setup. Algorithmic frameworks with invariant steps but variable details (e.g., parsing files).

Trade-off 2: Spring Bean Map Dispatch vs. Hardcoded Switch Blocks

Metric Spring Bean Map Dispatch Hardcoded Switch Block
Code Maintenance Clean. Adding a strategy requires zero modifications to existing classes. Poor. Adding a strategy forces modifying the switch statement.
Testing Isolation High. Mocking individual strategy beans is straightforward. Poor. Must mock the entire context class state.
Startup Overhead Medium. Requires scanning classpath during boot. Low. Instant compile-time parsing with zero overhead.

Staff Engineer Perspective

Operating dynamic strategy routing engines in production requires stateless constructs and optimized mapping.

// Thread-Safe Atomic Map Reference Swap
public class AtomicStrategyRouter {
    private final AtomicReference<Map<String, PaymentStrategy>> strategyMap = 
        new AtomicReference<>(new ConcurrentHashMap<>());

    public void updateStrategies(Map<String, PaymentStrategy> newMap) {
        // Atomic reference swap prevents locking overhead
        strategyMap.set(newMap);
    }

    public void routePayment(String merchantId, double amount) {
        PaymentStrategy strategy = strategyMap.get().get(merchantId);
        if (strategy == null) {
            strategy = strategyMap.get().get("DEFAULT");
        }
        strategy.pay(amount);
    }
}

Verbal Script

Interviewer: "How do you implement the Strategy pattern in Spring Boot to avoid switch-case blocks when resolving different payment methods at runtime?"

Candidate: "In Spring Boot, I implement the Strategy pattern by combining interface polymorphism with Spring's dependency injection container.

First, I define a common PaymentStrategy interface with a pay method and a getStrategyName identifier method.

Next, I implement this interface across multiple classes, such as StripeStrategy, PaypalStrategy, and AdyenStrategy, marking each class with the @Component annotation.

Inside the core PaymentService class—which acts as the context—I inject all beans implementing PaymentStrategy directly into a map:

private final Map<String, PaymentStrategy> strategies;

By using constructor injection, Spring automatically autowires this map, where the map keys are the bean name identifiers (e.g. 'stripeStrategy') and the values are the strategy instances.

When a payment request arrives containing a method key, the service retrieves the strategy instance directly from the map:

PaymentStrategy strategy = strategies.get(methodKey);

If the strategy exists, it executes the payment; otherwise, it throws an exception or defaults to a fallback bean.

This approach completely eliminates switch-case blocks and satisfies the Open/Closed Principle.

We can add new payment methods simply by writing a new bean class, without modifying the existing service code."


Interviewer: "What are the trade-offs between using the Strategy pattern (composition) and the Template Method pattern (inheritance)?"

Candidate: "The primary trade-offs center around binding time, flexibility, and code complexity.

The Strategy Pattern uses composition.

It defines a helper interface that is injected into a context class.

Because it relies on composition, we can change the strategy dynamically at runtime—for example, switching a routing gateway from FedEx to UPS if an API call fails.

However, it introduces minor complexity by requiring separate class declarations for every algorithm variant and slightly more object allocations on the heap.

The Template Method uses inheritance.

It defines the skeleton of an algorithm in an abstract base class, allowing subclasses to override specific steps.

Because it uses inheritance, the algorithm structure is statically bound at compile-time and cannot be altered at runtime.

It is simpler because it requires fewer classes, but it makes testing more difficult and leads to a fragile class hierarchy—changes in the base class can inadvertently break subclass logic.

I prefer Strategy because composition provides better isolation, is easier to unit test, and aligns with the clean object-oriented principle of choosing composition over inheritance."


Interviewer: "How do you handle thread safety and GC allocation overhead in a high-concurrency environment when utilizing dynamic strategy routing?"

Candidate: "To handle thread safety under high concurrency, I design all strategy beans to be completely stateless.

They do not contain any instance variables that change per request.

All transaction data, such as amounts and routing keys, is passed directly through the method parameters on the stack, which is local to the executing thread.

This stateless design allows us to run these strategies as Singletons.

We instantiate them once during application bootstrap and cache them in a map, allowing thousands of threads to execute their methods concurrently without thread contention.

By using singletons, we also resolve the garbage collection overhead.

If we instantiated a new strategy class for every transaction, a system handling 50,000 transactions per second would allocate gigabytes of short-lived objects on the heap every minute, triggering frequent Minor GC pauses.

By caching stateless singletons, the heap allocation rate for strategy routing drops to zero, protecting the JVM from performance degradation."


Want to track your progress?

Sign in to save your progress, track completed lessons, and pick up where you left off.