System Design (LLD) Full Course: Strategy Design Pattern Explained with Real-World Example
A robust application design must accommodate continuous evolution. As new feature requirements emerge, software architecture should remain flexible enough to integrate changes with minimal time and minimal code modifications. This lesson explores the Strategy Design Pattern, demonstrating how separating dynamic, frequently changing code from static logic eliminates deep inheritance trees and produces maintainable, production-ready systems.
1. Introduction to Design Patterns & Software Evolution
In software engineering, developers repeatedly encounter recurring architectural challenges. Rather than reinventing solutions, industry practitioners rely on Design Patterns—standardized, battle-tested architectural blueprints derived from collective engineering wisdom.
The Fundamental Philosophy of Design Patterns
Although there are 23 official Gang of Four (GoF) design patterns, they all fundamentally address one core principle:
Identify the parts of your application that vary (dynamic code) and separate/extract them from the parts that remain standard (static code).
By encapsulating dynamic behavior into separate classes, modifications to changing requirements will not break or regress the static core of the application.
The Immutable Law of Software Engineering
- Change is the only constant: Applications will continuously evolve over time.
- Flexible Design Standard: A design is considered flexible if adding a new feature requires minimal code changes and minimal turnaround time without violating architectural integrity.
2. Problem Statement: Building a Robot Simulation System
To understand why traditional approaches break down, consider an application designed to simulate various types of robots moving and interacting within an environment.
Initial Naive Design Model
We begin by defining a base abstract class Robot with concrete implementations for common behaviors (walk and talk) and an abstract method for visualization (projection).
Code Representation (Initial Setup)
public abstract class Robot {
public void walk() {
System.out.println("Walking on two legs...");
}
public void talk() {
System.out.println("Speaking default language...");
}
public abstract void projection();
}
public class CompanionRobot extends Robot {
@Override
public void projection() {
System.out.println("Displaying friendly humanoid hologram.");
}
}
public class WorkerRobot extends Robot {
@Override
public void projection() {
System.out.println("Displaying heavy-duty industrial chassis.");
}
}
Initially, this structure works cleanly. However, as new types of robots with distinct capabilities are introduced, this model fails.
3. The Inheritance Trap & The Permutation Nightmare
Now consider extending the application to support flying robots, such as a SparrowRobot and a CrowRobot.
Attempt 1: Class-Level Duplication (Violating DRY)
If we add a fly() method directly inside SparrowRobot and CrowRobot:
public class SparrowRobot extends Robot {
public void fly() {
System.out.println("Flying with flapping wings...");
}
@Override
public void projection() { System.out.println("Sparrow shape"); }
}
public class CrowRobot extends Robot {
public void fly() {
System.out.println("Flying with flapping wings..."); // Duplicate Code!
}
@Override
public void projection() { System.out.println("Crow shape"); }
}
- Failure: Violates the DRY (Do Not Repeat Yourself) principle. The identical flying algorithm is duplicated across multiple classes.
Attempt 2: Pushing Behavior Up to Base Class
To prevent duplication, we might push fly() into the base Robot class.
- Failure: Non-flying robots like
CompanionRobotandWorkerRobotinheritfly(), causing invalid runtime states (e.g., ground worker robots floating in the air).
Attempt 3: Deepening the Inheritance Hierarchy
To isolate flying capabilities, we might create intermediate subclasses: FlyableRobot and NonFlyableRobot.
Now suppose new variations emerge:
JetRobotflies using jet thrusters instead of wings (FlyWithJetvsFlyWithWings).- Some robots cannot walk (
NonWalkable). - Some robots cannot speak (
NonTalkable).
Attempting to model these variations via class inheritance triggers a combinatorial explosion of classes:
Crucial Engineering Insight: "The solution to inheritance issues is NEVER more inheritance." Deep inheritance structures cause tight coupling, massive code duplication, and blatant violations of the Open/Closed Principle (OCP).
4. Understanding the Strategy Design Pattern
The Strategy Design Pattern resolves inheritance bottlenecks by favoring Composition over Inheritance.
Formal Definition
Strategy Pattern defines a family of algorithms, encapsulates each one inside a separate class, and makes them interchangeable at runtime. Strategy lets the algorithm vary independently from clients that use it.
Core Architectural Shift
Instead of inheriting behavior from a parent class, the Robot class delegates behavioral tasks to specialized strategy objects.
5. Architectural Blueprint & Structural Design
To implement the Strategy Pattern for the robot simulation system, dynamic behaviors (Walking, Talking, and Flying) are extracted into dedicated strategy interface families.
1. Strategy Interfaces
Note: In languages like Java, strategies are declared using
interface. In C++, pure abstract classes with pure virtual functions are used.
WalkableStrategy: Interface encapsulating walking logic.TalkableStrategy: Interface encapsulating speech logic.FlyableStrategy: Interface encapsulating flight logic.
2. Concrete Strategy Classes
Each behavior variation is isolated into its own concrete strategy implementation:
- Fly Strategies:
NormalFly(wings),JetFly(jets),NoFly(disabled). - Walk Strategies:
NormalWalk,NoWalk. - Talk Strategies:
NormalTalk,NoTalk.
Mental Hook: Even doing nothing (e.g.,
NoFly,NoTalk) is a concrete behavior strategy.
3. Context Class (Robot)
The Robot class maintains interface references to these dynamic strategy instances and delegates method execution directly to them.
6. Complete Code Implementation (Java)
Step 1: Strategy Interfaces & Concrete Implementations
// =---------------------------------------------------
// Flying Strategy Family
// =---------------------------------------------------
public interface FlyableStrategy {
void fly();
}
public class NormalFly implements FlyableStrategy {
@Override
public void fly() {
System.out.println("Flying gracefully with wings.");
}
}
public class JetFly implements FlyableStrategy {
@Override
public void fly() {
System.out.println("Flying at supersonic speeds using jet thrusters!");
}
}
public class NoFly implements FlyableStrategy {
@Override
public void fly() {
System.out.println("Cannot fly. Grounded.");
}
}
// =---------------------------------------------------
// Walking Strategy Family
// =---------------------------------------------------
public interface WalkableStrategy {
void walk();
}
public class NormalWalk implements WalkableStrategy {
@Override
public void walk() {
System.out.println("Walking normally on two legs.");
}
}
public class NoWalk implements WalkableStrategy {
@Override
public void walk() {
System.out.println("Stationary unit. Cannot walk.");
}
}
// =---------------------------------------------------
// Talking Strategy Family
// =---------------------------------------------------
public interface TalkableStrategy {
void talk();
}
public class NormalTalk implements TalkableStrategy {
@Override
public void talk() {
System.out.println("Communicating via natural voice synthesized audio.");
}
}
public class NoTalk implements TalkableStrategy {
@Override
public void talk() {
System.out.println("<Silent Mode: Cannot speak>");
}
}
Step 2: Refactored Base Context Class (Robot)
public abstract class Robot {
// Composition: Robot HAS-A references to behavioral strategies
protected WalkableStrategy walkableStrategy;
protected TalkableStrategy talkableStrategy;
protected FlyableStrategy flyableStrategy;
public Robot(WalkableStrategy w, TalkableStrategy t, FlyableStrategy f) {
this.walkableStrategy = w;
this.talkableStrategy = t;
this.flyableStrategy = f;
}
// Delegated actions (Dumb delegator methods)
public void performWalk() {
walkableStrategy.walk();
}
public void performTalk() {
talkableStrategy.talk();
}
public void performFly() {
flyableStrategy.fly();
}
// Setter methods allow dynamic runtime behavioral changes
public void setFlyableStrategy(FlyableStrategy f) {
this.flyableStrategy = f;
}
public abstract void projection();
}
Step 3: Concrete Subclasses & Application Execution
public class CompanionRobot extends Robot {
public CompanionRobot(WalkableStrategy w, TalkableStrategy t, FlyableStrategy f) {
super(w, t, f);
}
@Override
public void projection() {
System.out.println("Projecting friendly companion interface.");
}
}
public class Main {
public static void main(String[] args) {
// Constructing a companion robot that walks, talks, but cannot fly
Robot companion = new CompanionRobot(
new NormalWalk(),
new NormalTalk(),
new NoFly()
);
System.out.println("--- Companion Robot Execution ---");
companion.projection();
companion.performWalk();
companion.performTalk();
companion.performFly();
// Dynamically upgrading companion robot with Jet Thrusters at runtime!
System.out.println("\n--- Upgrading Companion Robot with Jet Pack ---");
companion.setFlyableStrategy(new JetFly());
companion.performFly();
}
}
7. Advanced Refactoring: Eliminating Class Inheritance Completely
Although we decoupled behaviors using composition, Robot still relies on subclass inheritance (CompanionRobot, WorkerRobot) for the projection() method.
The Pure Strategy Approach
We can eliminate class inheritance entirely by turning visual projection into a strategy (ProjectableStrategy):
public interface ProjectableStrategy {
void project();
}
public class CompanionProjection implements ProjectableStrategy {
@Override
public void project() {
System.out.println("Projecting friendly companion avatar.");
}
}
public class WorkerProjection implements ProjectableStrategy {
@Override
public void project() {
System.out.println("Projecting industrial worker chassis.");
}
}
Now, the Robot class no longer needs to be abstract:
public class Robot {
private WalkableStrategy walkStrategy;
private TalkableStrategy talkStrategy;
private FlyableStrategy flyStrategy;
private ProjectableStrategy projectStrategy;
public Robot(WalkableStrategy w, TalkableStrategy t, FlyableStrategy f, ProjectableStrategy p) {
this.walkStrategy = w;
this.talkStrategy = t;
this.flyStrategy = f;
this.projectStrategy = p;
}
public void walk() { walkStrategy.walk(); }
public void talk() { talkStrategy.talk(); }
public void fly() { flyStrategy.fly(); }
public void projection() { projectStrategy.project(); }
}
Instantiating Any Robot Configuration Without Subclasses
// Instantiating a Jet-Powered Worker Robot purely via Composition
Robot jetWorker = new Robot(
new NormalWalk(),
new NoTalk(),
new JetFly(),
new WorkerProjection()
);
Subclass hierarchies are completely removed, replaced by fully modular strategy configurations.
8. Standard UML Class Diagram Workflow
Below is the standard structural flow of the Strategy Design Pattern:
Workflow Tracing:
- Context (
Robot) contains a reference variable pointing to the Strategy interface. - The Client initializes the Context with a specific Concrete Strategy implementation (
JetFly,NormalWalk). - When the Client triggers Context actions (
performFly()), the Context delegates execution directly tostrategy.fly().
9. Real-World Enterprise Applications
The Strategy Pattern is widely used in enterprise production frameworks.
Example 1: Enterprise Payment Gateway System
A checkout service must support multiple payment processors (UPI, Credit/Debit Cards, NetBanking).
Implementation Architecture:
public interface PaymentStrategy {
void processPayment(double amount);
}
public class UpiPayment implements PaymentStrategy {
@Override
public void processPayment(double amount) {
System.out.println("Paid $" + amount + " using UPI Virtual Payment Address.");
}
}
public class CardPayment implements PaymentStrategy {
@Override
public void processPayment(double amount) {
System.out.println("Paid $" + amount + " using Credit/Debit Card.");
}
}
public class CheckoutService {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
}
public void checkout(double amount) {
paymentStrategy.processPayment(amount);
}
}
Example 2: Data Sorting Engine Framework
An analytical platform selecting sorting algorithms based on dataset size and memory constraints.
public interface SortingStrategy {
void sort(int[] data);
}
public class QuickSortStrategy implements SortingStrategy {
@Override
public void sort(int[] data) {
System.out.println("Sorting dataset using QuickSort O(N log N)...");
}
}
public class MergeSortStrategy implements SortingStrategy {
@Override
public void sort(int[] data) {
System.out.println("Sorting dataset using MergeSort (Stable)...");
}
}
public class DataProcessor {
private SortingStrategy sorter;
public DataProcessor(SortingStrategy sorter) {
this.sorter = sorter;
}
public void process(int[] numbers) {
sorter.sort(numbers);
}
}
Comparative Domain Matrix
10. Summary and Key Takeaways
- Core Purpose: The Strategy Design Pattern encapsulates a family of algorithms into interchangeable classes, allowing runtime behavioral modification without altering client context code.
- Separation of Concerns: Always separate dynamic, volatile code logic from core static code logic.
- Composition over Inheritance: Favor "Has-A" relationships (composition) over "Is-A" relationships (inheritance) to avoid complex, deeply nested class hierarchies.
- DRY Principle Adherence: Prevents code duplication across concrete sub-classes by centralizing algorithm logic inside standalone strategy classes.
- Open/Closed Principle (OCP): New algorithms or behaviors can be added seamlessly by implementing the strategy interface, requiring zero modifications to existing classes.
- Runtime Flexibility: Context behaviors can be swapped dynamically on the fly using setter methods (e.g., swapping
NoFlywithJetFly). - Interviews & LLD Relevance: The Strategy Pattern is fundamental in Low-Level Design (LLD) interviews and serves as the building block for modern enterprise architecture.