design

Factory + Strategy Design Patterns in Java

A guide on how to combine Factory and Strategy patterns to build flexible and maintainable systems.

Reading Time: 12 min readAuthor: DeepTechHub
#design-patterns#factory-pattern#strategy-pattern#java#architecture
Factory + Strategy Design Patterns in Java

Audio controls for Factory + Strategy Design Patterns in Java

Listen to this article

Design patterns are most valuable when they solve a real design problem rather than existing in isolation. One of the most common and useful combinations is Factory + Strategy.

The Strategy Pattern encapsulates multiple algorithms behind a common interface, while the Factory Pattern selects the appropriate implementation. Together they eliminate large if-else or switch statements and make applications easier to extend.

This combination appears frequently in payment processing, authentication, notification systems, pricing engines, file exporters, and many Spring Boot applications.


The Problem

Suppose an application supports multiple payment methods.

A straightforward implementation often looks like this:

public void checkout(PaymentMethod method, BigDecimal amount) {
    switch (method) {
        case CARD -> processCard(amount);
        case CASH -> processCash(amount);
        case DIGITAL_WALLET -> processWallet(amount);
    }
}

Initially this works well.

As the application grows, the method becomes responsible for every payment implementation, making it harder to maintain and violating the Open/Closed Principle.

Instead of embedding every algorithm inside checkout(), we can move each payment implementation into its own class.


Step 1 – Strategy Pattern

The Strategy Pattern defines a family of interchangeable algorithms.

public interface PaymentStrategy {
    void pay(BigDecimal amount);
}

Each payment method becomes an independent implementation.

public class CardPayment implements PaymentStrategy {
 
    @Override
    public void pay(BigDecimal amount) {
        System.out.println("Paid " + amount + " using Card");
    }
}
public class CashPayment implements PaymentStrategy {
 
    @Override
    public void pay(BigDecimal amount) {
        System.out.println("Paid " + amount + " using Cash");
    }
}
public class DigitalWalletPayment implements PaymentStrategy {
 
    @Override
    public void pay(BigDecimal amount) {
        System.out.println("Paid " + amount + " using Digital Wallet");
    }
}

The checkout service now depends only on the interface.

public class CheckoutService {
 
    public void checkout(BigDecimal amount,
                         PaymentStrategy strategy) {
 
        strategy.pay(amount);
    }
}

Usage:

CheckoutService checkout = new CheckoutService();
 
checkout.checkout(
        new BigDecimal("99.99"),
        new CardPayment());

Notice that CheckoutService has no knowledge of concrete payment implementations.

It simply executes the supplied strategy.


Where Strategy Falls Short

Although Strategy removes conditional logic from the checkout process, the client is still responsible for creating the correct implementation.

checkout.checkout(amount, new CardPayment());

Somewhere in the application we still need logic such as

if (...) {
    new CardPayment();
}

or

switch (...) {
}

This is exactly where the Factory Pattern helps.


Step 2 – Factory Pattern

The factory centralizes object creation.

public enum PaymentMethod {
    CARD,
    CASH,
    DIGITAL_WALLET
}
public class PaymentStrategyFactory {
 
    public static PaymentStrategy create(PaymentMethod method) {
 
        return switch (method) {
 
            case CARD -> new CardPayment();
 
            case CASH -> new CashPayment();
 
            case DIGITAL_WALLET -> new DigitalWalletPayment();
        };
    }
}

Now the client no longer creates strategy objects directly.

PaymentStrategy strategy =
        PaymentStrategyFactory.create(PaymentMethod.CARD);
 
checkout.checkout(
        new BigDecimal("99.99"),
        strategy);

Responsibilities are now clearly separated.

PatternResponsibility
StrategyDefines how the payment is performed
FactoryDecides which strategy to create

Factory + Strategy Working Together

The complete flow is now

Client
   │
   ▼
PaymentStrategyFactory
   │
   ▼
Concrete Strategy
   │
   ▼
CheckoutService

The client only specifies what it wants.

The factory decides which implementation to instantiate.

The strategy decides how the work is performed.

Each class has a single responsibility.


Adding a New Payment Method

Suppose tomorrow the business adds cryptocurrency support.

Create a new strategy.

public class CryptoPayment implements PaymentStrategy {
 
    @Override
    public void pay(BigDecimal amount) {
        System.out.println("Paid " + amount + " using Crypto");
    }
}

Register it inside the factory.

case CRYPTO -> new CryptoPayment();

No changes are required in CheckoutService.

The payment algorithm remains completely independent from the code that uses it.


Production Applications (Spring Boot)

In many Spring Boot applications, the factory becomes much simpler because Spring already creates all strategy implementations.

@Component
public class PaymentStrategyFactory {
 
    private final Map<String, PaymentStrategy> strategies;
 
    public PaymentStrategyFactory(
            Map<String, PaymentStrategy> strategies) {
 
        this.strategies = strategies;
    }
 
    public PaymentStrategy get(String type) {
        return strategies.get(type);
    }
}

Spring automatically discovers every PaymentStrategy bean and injects them into the map.

Adding a new strategy often requires nothing more than creating another implementation annotated with @Component.


When to Use This Combination

Factory + Strategy works well when:

  • Multiple interchangeable algorithms exist.
  • The algorithm is chosen at runtime.
  • Each implementation has its own dependencies.
  • New implementations are added frequently.
  • You want to follow the Open/Closed Principle.

Typical examples include:

  • Payment processing
  • Authentication providers
  • Notification channels
  • File exporters
  • Pricing engines
  • Discount calculation
  • Shipping providers

When Not to Use It

Avoid this combination when there is only one implementation or the behavior is extremely simple.

For lightweight scenarios, an enum, lambda expression, or a simple switch statement is often easier to understand.

Design patterns should reduce complexity—not introduce it.


Summary

Factory and Strategy solve different problems.

  • Strategy encapsulates different algorithms behind a common interface.
  • Factory centralizes object creation.
  • Together they remove conditional logic, improve extensibility, and keep responsibilities separated.

This combination is one of the most frequently used design techniques in enterprise Java applications because it produces code that is easier to test, easier to extend, and easier to maintain as requirements evolve.

Did you find this article useful?