Strategy Design Pattern
In the world of software development, designing code for efficiency, reusability, and minimal complexity is paramount. One of the most effective methods for achieving this is through the use of design patterns. This article will shine a light on one specific example: “STRATEGY DESIGN PATTERN”.
So what is Strategy Design Pattern?
It is one of the behavioral design patterns where it allows you to dynamically change the behaviour of an object by encapsulating it into different strategies.
This pattern enables you to modify the strategy of an object by choosing one among multiple strategies at runtime or dynamically rather than having a single one.
It is based on the principle of composition over inheritence.
The core idea behind this pattern is we decouple the core logic from the object and encapsulating those into a seperate class and injecting it into the object in a loose manner there by allowing us to have multiple strategies to implement the same core logic.
This enables the object to be more flexible and reusable as different strategies can be added or modified without modying the core object code
Components of the Strategy Design Pattern:
— — — — — — — — — — — — — — — — — — — — — — —
1. Context: The context is nothing but an object that contains the reference to a strategy object and interacts with it through the common interface. We will be creating this context object by passing the required strategy to execute the respective behaviour
2. Strategy Interface: The interface that defines the behaviour of all strategies.The strategies implement this interface by providing its unique implementation of the behaviour
3. Concrete Strategies: The classes that implement the strategy interface and encapsulating a specific behaviour that the context can switch at runtime
Benefits:
— — — —
1) Improved code flexibility: By encapsualting the behaviour of object into multiple strategies ,the code becomes more flexible and easier to modify
2) Better Code Resusability: As we are encapsualting the behaviour into multiple strategies we can reuse them in different objects according to the requirement
3) Follows few of the SOLID principles like Open/Closed where this pattern allows us to add more options by following the open principle and restricitng the changes by modifying the existing code
4) Tests got simplified: As we have move the behaviour to other classes we can easily test those strategies making it easy for the developers
There are multiple Use cases present around for implementing this pattern, below are the few
1. Different types of validation needs to be performed based on the type of user
2. Different kinds of payments needs to allowed for the user to performed
3. Different kinds of access management can be performed accordingly
Real world Scenario:
— — — — — — — — — —
Here we will consider the example of processing a Payment where we have different payment types to process the payments
As we know while performing a payment a user can do it in multiple ways like Debit, Credit, Upi etc
first we will see how the code will be there if we dont have a strategy
Get Prassu’s stories in your inbox
Join Medium for free to get updates from this writer.
public class PaymentProcessor {
private PaymentType paymentType;
public void processPayment(double amount) {
if (paymentType == PaymentType.CREDIT_CARD) {
System.out.println(“Processing credit card payment of amount “ + amount);
} else if (paymentType == PaymentType.DEBIT_CARD) {
System.out.println(“Processing debit card payment of amount “ + amount);
} else if (paymentType == PaymentType.UPI) {
System.out.println(“Processing UPI payment of amount “ + amount);
} else {
throw new IllegalArgumentException(“Invalid payment type”);
}
}
public void setPaymentType(PaymentType paymentType) {
this.paymentType = paymentType;
}
}
enum PaymentType {
CREDIT_CARD,
DEBIT_CARD,
UPI
}
In the above code if we see, the core logic needs to be modified if we have to add any new payment method which violates the Open/Closed priciple (one of SOLID principles) where the class needs to be open for extension but closed for modification, but here due to new payment gateway or channel we need to modify the code.
Also we need to perform checks in order to identify what kind of payment method we need to process the payment and it will become difficult for the developer to manage the code properly if the payment methods keep on increasing
To fix this we will follow the Strategy Design pattern with the below steps
1. We will create the Strategy interface which is one of the core component of this pattern
public interface PaymentStrategy {
void processPayment(double amount);
}
2. We will implement the concrete strategies by implementing this strategy interface like CreditCardPaymentStrategy, DebitCardPaymentStrategy, UPIPaymentStrategy etc.,
public class CreditCardPaymentStrategy implements PaymentStrategy {
public void processPayment(double amount) {
System.out.println(“Processing credit card payment of amount “ + amount);
}
}
public class DebitCardPaymentStrategy implements PaymentStrategy {
public void processPayment(double amount) {
System.out.println(“Processing debit card payment of amount “ + amount);
}
}
public class UPIPaymentStrategy implements PaymentStrategy {
public void processPayment(double amount) {
System.out.println(“Processing UPI payment of amount “ + amount);
}
}
3. In the final step you implement the payment processor such a way that it takes the payment strategy as a dependency where at runtime you can able to provide its concrete implementation
public class PaymentProcessor {
private PaymentStrategy paymentStrategy;
public PaymentProcessor(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
public void processPayment(double amount) {
paymentStrategy.processPayment(amount);
}
}
public class PerformPayment {
public static void main(String[] args) {
PaymentProcessor paymentProcessor = new PaymentProcessor(new CreditCardPaymentStrategy());
paymentProcessor.processPayment(100);
PaymentProcessor paymentProcessor1 = new PaymentProcessor(new DebitCardPaymentStrategy());
paymentProcessor1.processPayment(100);
}
}
Another Real world Scenario :
— — — — — — — — — — — — — — -
We have usecase where we need to access a file to read/write, but in multi thread environment we need to have a lock in order to allow only one thread to access the file to avoid contention while writing content into the file and allowing multiple threads to access the file while reading.
Generally to do so we will be using the synchronized keyword which uses intrinsic locks or locks from concurrent package, in which we extensively use locks for fine control over the objects. In reality we develop the code with one kind of lock like ReentrantLock and we identified it is better to use different kind of lock like ReadWriteLocks or other kind of lock after having the performance issues.
In this case we will be using a pattern called “Strategized locking pattern”
This pattern is a specialization of the Strategy design pattern applied to concurrency control where in future we can able to change the lock mechanism or if we have any upgraded lock which performs better and provided by third party library we can easily integrate it without modifying the core logic
Core Components for this Use Case:
— — — — — — — — — — — — — — — — —
It involves three primary parts similar to the Strategy pattern
1. The Lockable Object: An interface that defines the operations for locking a shared resource.
2. The lock wrapper (or strategy): An implementation of the locking mechanism, such as a ReentrantLock or a ReadWriteLock, that provides a consistent way to acquire and release the lock.
3. The guard: An object (often a class, or more simply, a try-finally block) that is responsible for acquiring and releasing the lock automatically within a specific scope. This ensures that the lock is always released, even if an exception occurs.
By using the above the three parts we will design the locking mechanism in the following steps
1. We create a Lockable interface that contains the required methods or functionalities that we want to perform on that object . This is the strategy interface here.
interface Lockable {
void acquire();
void release();
}
2. Now we will implement the concrete strategies by implementing this interface with different kinds of behaviour
class ThreadMutex implements Lockable {
private final java.util.concurrent.locks.ReentrantLock lock = new java.util.concurrent.locks.ReentrantLock();
public void acquire() { lock.lock(); }
public void release() { lock.unlock(); }
}
class NullLock implements Lockable {
public void acquire() {}
public void release() {}
}
3. Implementing the Guard
// Generic Guard (Scoped Lock)
class Guard<L extends Lockable> implements AutoCloseable {
private final L lock;
public Guard(L lock) { this.lock = lock; lock.acquire(); }
public void close() { lock.release(); }
}
class FileCache<L extends Lockable> {
private final L lock;
public FileCache(L lock) { this.lock = lock; }
public String find(String pathname) throws Exception{
try (Guard<L> guard = new Guard<>(lock)) {
// Critical section
return “file”;
}
}
}
Usage:
— — —
FileCache<ThreadMutex> multiThreadedCache = new FileCache<>(new ThreadMutex());
FileCache<NullLock> singleThreadedCache = new FileCache<>(new NullLock());
After using this pattern we can see that the code is flexible and can be resuable which allows us to add any new kind of lock in future but downside is we have increased the complexity due to extra abstraction.
Ultimately, the value of the Strategy Design Pattern lies in its ability to simplify complexity and increase flexibility. When you face a situation with multiple behavioral options, ask yourself: “Can I use the Strategy Pattern here or not?”. Embracing this mindset will help you write code that is not only functional today but also resilient to the inevitable changes of tomorrow.
Thank you for reading and let me know your feedback for improving my articles



