Skip to main content

Factory Pattern: From Scattered If-Else Hell to Zero Conditionals

Β· 12 min read
Mahmut Salman
Software Developer

"But don't we still have if-else statements with interfaces?" Yes! But here's the critical difference: scattered if-else in 10 files vs centralized if-else in 1 factory vs zero if-else with registry pattern. Let's see the evolution.

The Question​

"Using interfaces with strategy pattern is great, but don't we still have if-else statements to choose which implementation to use? Aren't we just moving the problem?"

Great observation! Yes, conditionals still exist. But let's see the massive difference in where they live and how they evolve.


Level 0: No Interfaces (If-Else Hell)​

The Nightmare Scenario​

Your e-commerce app needs product sorting in multiple places:

ProductListView.java:

public class ProductListView {

public void displayProducts(String sortType) {
List<Product> sorted;

if (sortType.equals("price_asc")) {
sorted = products.stream()
.sorted(Comparator.comparingDouble(Product::getPrice))
.collect(Collectors.toList());

} else if (sortType.equals("price_desc")) {
sorted = products.stream()
.sorted(Comparator.comparingDouble(Product::getPrice).reversed())
.collect(Collectors.toList());

} else if (sortType.equals("rating")) {
sorted = products.stream()
.sorted(Comparator.comparingDouble(Product::getRating).reversed())
.collect(Collectors.toList());

} else {
sorted = products; // Default: no sorting
}

render(sorted);
}
}

RecommendationEngine.java:

public class RecommendationEngine {

public List<Product> getRecommendations(String sortType) {
List<Product> recommendations = findRecommendations();

// Same if-else duplicated!
if (sortType.equals("price_asc")) {
return recommendations.stream()
.sorted(Comparator.comparingDouble(Product::getPrice))
.collect(Collectors.toList());

} else if (sortType.equals("price_desc")) {
return recommendations.stream()
.sorted(Comparator.comparingDouble(Product::getPrice).reversed())
.collect(Collectors.toList());

} else if (sortType.equals("rating")) {
return recommendations.stream()
.sorted(Comparator.comparingDouble(Product::getRating).reversed())
.collect(Collectors.toList());

} else {
return recommendations;
}
}
}

AdminPanel.java:

public class AdminPanel {

public void generateProductReport(String sortType) {
List<Product> products = productRepository.findAll();

// Same if-else again!
if (sortType.equals("price_asc")) {
products = products.stream()
.sorted(Comparator.comparingDouble(Product::getPrice))
.collect(Collectors.toList());

} else if (sortType.equals("price_desc")) {
products = products.stream()
.sorted(Comparator.comparingDouble(Product::getPrice).reversed())
.collect(Collectors.toList());

} else if (sortType.equals("rating")) {
products = products.stream()
.sorted(Comparator.comparingDouble(Product::getRating).reversed())
.collect(Collectors.toList());
}

generateReport(products);
}
}

ReportGenerator.java:

public class ReportGenerator {

public Report createSalesReport(String sortType) {
List<Product> products = getSalesData();

// Same if-else AGAIN!
if (sortType.equals("price_asc")) {
products = products.stream()
.sorted(Comparator.comparingDouble(Product::getPrice))
.collect(Collectors.toList());

} else if (sortType.equals("price_desc")) {
products = products.stream()
.sorted(Comparator.comparingDouble(Product::getPrice).reversed())
.collect(Collectors.toList());

} else if (sortType.equals("rating")) {
products = products.stream()
.sorted(Comparator.comparingDouble(Product::getRating).reversed())
.collect(Collectors.toList());
}

return new Report(products);
}
}

The Problem​

Code duplication:

Same if-else block: 4 files
Same sorting logic: 4 places
Total lines of conditional code: 60+ lines

What happens when you add "trending" sort?

1. Update ProductListView.java
2. Update RecommendationEngine.java
3. Update AdminPanel.java
4. Update ReportGenerator.java
5. Hope you didn't miss any file!
6. Risk of inconsistent implementations

Maintenance nightmare:

  • ❌ 4 places to update for every new sort
  • ❌ Easy to forget one place
  • ❌ Inconsistent implementations (copy-paste errors)
  • ❌ Hard to test sorting independently
  • ❌ Violates DRY principle

Level 1: Interfaces with Factory (Centralized If-Else)​

Step 1: Define Interface​

public interface SortingStrategy {
List<Product> sort(List<Product> products);
}

Step 2: Create Implementations​

public class PriceLowToHighSort implements SortingStrategy {
@Override
public List<Product> sort(List<Product> products) {
return products.stream()
.sorted(Comparator.comparingDouble(Product::getPrice))
.collect(Collectors.toList());
}
}

public class PriceHighToLowSort implements SortingStrategy {
@Override
public List<Product> sort(List<Product> products) {
return products.stream()
.sorted(Comparator.comparingDouble(Product::getPrice).reversed())
.collect(Collectors.toList());
}
}

public class RatingSort implements SortingStrategy {
@Override
public List<Product> sort(List<Product> products) {
return products.stream()
.sorted(Comparator.comparingDouble(Product::getRating).reversed())
.collect(Collectors.toList());
}
}

Step 3: Centralize If-Else in Factory​

public class SortingStrategyFactory {

public static SortingStrategy getStrategy(String sortType) {
// If-else exists, but ONLY HERE!
if (sortType.equals("price_asc")) {
return new PriceLowToHighSort();

} else if (sortType.equals("price_desc")) {
return new PriceHighToLowSort();

} else if (sortType.equals("rating")) {
return new RatingSort();

} else {
throw new IllegalArgumentException("Unknown sort type: " + sortType);
}
}
}

Step 4: Simplify All Calling Code​

ProductListView.java:

public class ProductListView {

public void displayProducts(String sortType) {
// One line replaces 20 lines of if-else!
SortingStrategy strategy = SortingStrategyFactory.getStrategy(sortType);
List<Product> sorted = strategy.sort(products);
render(sorted);
}
}

RecommendationEngine.java:

public class RecommendationEngine {

public List<Product> getRecommendations(String sortType) {
List<Product> recommendations = findRecommendations();

// Same simple code everywhere
SortingStrategy strategy = SortingStrategyFactory.getStrategy(sortType);
return strategy.sort(recommendations);
}
}

AdminPanel.java:

public class AdminPanel {

public void generateProductReport(String sortType) {
List<Product> products = productRepository.findAll();

SortingStrategy strategy = SortingStrategyFactory.getStrategy(sortType);
List<Product> sorted = strategy.sort(products);

generateReport(sorted);
}
}

ReportGenerator.java:

public class ReportGenerator {

public Report createSalesReport(String sortType) {
List<Product> products = getSalesData();

SortingStrategy strategy = SortingStrategyFactory.getStrategy(sortType);
List<Product> sorted = strategy.sort(products);

return new Report(sorted);
}
}

The Improvement​

Before (scattered if-else):

If-else blocks: 4 files
Add new sort: Update 4 files
Risk of bugs: 4 places
Lines of conditional code: 60+

After (centralized factory):

If-else blocks: 1 file (factory)
Add new sort: Update 1 file
Risk of bugs: 1 place
Lines of conditional code: 15

What happens when you add "trending" sort?

// 1. Create new implementation
public class TrendingSort implements SortingStrategy {
@Override
public List<Product> sort(List<Product> products) {
return products.stream()
.sorted(Comparator.comparingInt(Product::getViewCount).reversed())
.collect(Collectors.toList());
}
}

// 2. Update factory (ONE place)
public class SortingStrategyFactory {
public static SortingStrategy getStrategy(String sortType) {
if (sortType.equals("price_asc")) {
return new PriceLowToHighSort();
} else if (sortType.equals("price_desc")) {
return new PriceHighToLowSort();
} else if (sortType.equals("rating")) {
return new RatingSort();
} else if (sortType.equals("trending")) { // ← Only change here!
return new TrendingSort();
}
throw new IllegalArgumentException("Unknown sort type: " + sortType);
}
}

// 3. Done! All 4 calling files automatically work!

Benefits:

  • βœ… Update ONE file instead of four
  • βœ… Consistent implementation everywhere
  • βœ… Easy to test each strategy independently
  • βœ… No risk of missing a file

Level 2: Registry Pattern (Eliminate If-Else)​

The Problem with Factory​

You still have to update the factory every time you add a new strategy. Can we do better?

Yes! Registry pattern.

The Registry​

public class SortingStrategyRegistry {

private static final Map<String, SortingStrategy> strategies = new HashMap<>();

// Register all strategies at startup
static {
strategies.put("price_asc", new PriceLowToHighSort());
strategies.put("price_desc", new PriceHighToLowSort());
strategies.put("rating", new RatingSort());
strategies.put("newest", new NewestFirstSort());
strategies.put("popular", new PopularSort());
}

public static SortingStrategy getStrategy(String sortType) {
SortingStrategy strategy = strategies.get(sortType);

if (strategy == null) {
throw new IllegalArgumentException("Unknown sort type: " + sortType);
}

return strategy;
}

// Allow dynamic registration
public static void registerStrategy(String name, SortingStrategy strategy) {
strategies.put(name, strategy);
}
}

No if-else! Just map lookup.

Adding New Strategy​

// Old way with factory - update if-else
if (sortType.equals("trending")) { // ← Add new condition
return new TrendingSort();
}

// New way with registry - just register
SortingStrategyRegistry.registerStrategy("trending", new TrendingSort());

Even Better: Self-Registering Strategies​

public abstract class SelfRegisteringStrategy implements SortingStrategy {

protected SelfRegisteringStrategy(String name) {
// Auto-register on creation
SortingStrategyRegistry.registerStrategy(name, this);
}
}

public class TrendingSort extends SelfRegisteringStrategy {

public TrendingSort() {
super("trending"); // Auto-registers as "trending"
}

@Override
public List<Product> sort(List<Product> products) {
return products.stream()
.sorted(Comparator.comparingInt(Product::getViewCount).reversed())
.collect(Collectors.toList());
}
}

// Usage: Just instantiate, it auto-registers!
new TrendingSort(); // Automatically available in registry

Level 3: Spring Auto-Discovery (Zero Configuration)​

The Ultimate Solution​

With Spring, strategies auto-register themselves - you write ZERO configuration code!

Step 1: Add getName() to Interface​

public interface SortingStrategy {
String getName(); // "price_asc", "rating", etc.
List<Product> sort(List<Product> products);
}

Step 2: Make Implementations Spring Components​

@Component
public class PriceLowToHighSort implements SortingStrategy {

@Override
public String getName() {
return "price_asc";
}

@Override
public List<Product> sort(List<Product> products) {
return products.stream()
.sorted(Comparator.comparingDouble(Product::getPrice))
.collect(Collectors.toList());
}
}

@Component
public class PriceHighToLowSort implements SortingStrategy {

@Override
public String getName() {
return "price_desc";
}

@Override
public List<Product> sort(List<Product> products) {
return products.stream()
.sorted(Comparator.comparingDouble(Product::getPrice).reversed())
.collect(Collectors.toList());
}
}

@Component
public class RatingSort implements SortingStrategy {

@Override
public String getName() {
return "rating";
}

@Override
public List<Product> sort(List<Product> products) {
return products.stream()
.sorted(Comparator.comparingDouble(Product::getRating).reversed())
.collect(Collectors.toList());
}
}

@Component
public class TrendingSort implements SortingStrategy {

@Override
public String getName() {
return "trending";
}

@Override
public List<Product> sort(List<Product> products) {
return products.stream()
.sorted(Comparator.comparingInt(Product::getViewCount).reversed())
.collect(Collectors.toList());
}
}

Step 3: Spring Auto-Injects ALL Implementations​

@Service
public class SortingStrategyRegistry {

private final Map<String, SortingStrategy> strategyMap = new HashMap<>();

// Spring automatically injects ALL implementations!
@Autowired
public SortingStrategyRegistry(List<SortingStrategy> allStrategies) {
// Build registry from all strategies
for (SortingStrategy strategy : allStrategies) {
strategyMap.put(strategy.getName(), strategy);
}
}

public SortingStrategy getStrategy(String sortType) {
SortingStrategy strategy = strategyMap.get(sortType);

if (strategy == null) {
throw new IllegalArgumentException("Unknown sort type: " + sortType);
}

return strategy;
}

public Set<String> getAvailableSortTypes() {
return strategyMap.keySet();
}
}

Step 4: Use It​

@Service
public class ProductService {

@Autowired
private SortingStrategyRegistry registry;

public List<Product> getSortedProducts(String sortType) {
SortingStrategy strategy = registry.getStrategy(sortType);
return strategy.sort(products);
}
}

Adding New Strategy (Zero Configuration!)​

// Just create a new @Component class - that's it!
@Component
public class DiscountSort implements SortingStrategy {

@Override
public String getName() {
return "discount"; // Auto-registers as "discount"
}

@Override
public List<Product> sort(List<Product> products) {
return products.stream()
.sorted(Comparator.comparingDouble(Product::getDiscountPercentage).reversed())
.collect(Collectors.toList());
}
}

// That's it! No factory update, no registry update!
// Spring automatically discovers it and adds it to the registry.

How it works:

  1. Spring scans for all @Component classes
  2. Finds all classes implementing SortingStrategy
  3. Injects them all into SortingStrategyRegistry constructor
  4. Registry builds map from strategy names
  5. New strategy? Just create @Component class - done!

The Evolution: Side-by-Side Comparison​

Level 0: No Interfaces (4 Updates)​

// 1. Update ProductListView.java
} else if (sortType.equals("trending")) {
sorted = products.stream()
.sorted(Comparator.comparingInt(Product::getViewCount).reversed())
.collect(Collectors.toList());
}

// 2. Update RecommendationEngine.java
} else if (sortType.equals("trending")) {
// Duplicate logic again...
}

// 3. Update AdminPanel.java
} else if (sortType.equals("trending")) {
// Duplicate logic again...
}

// 4. Update ReportGenerator.java
} else if (sortType.equals("trending")) {
// Duplicate logic again...
}

Files changed: 4 Risk of bugs: HIGH (might forget one file)

Level 1: Factory Pattern (1 Update)​

// 1. Create TrendingSort.java
public class TrendingSort implements SortingStrategy { ... }

// 2. Update SortingStrategyFactory.java
} else if (sortType.equals("trending")) {
return new TrendingSort();
}

Files changed: 2 (new class + factory) Risk of bugs: LOW (one place to update)

Level 2: Registry Pattern (1 Update, No If-Else)​

// 1. Create TrendingSort.java
public class TrendingSort extends SelfRegisteringStrategy {
public TrendingSort() {
super("trending"); // Auto-registers
}
...
}

// 2. Instantiate somewhere in app startup
new TrendingSort();

Files changed: 1 (just new class) No if-else modification needed

Level 3: Spring Auto-Discovery (1 Update, Zero Config)​

// 1. Create TrendingSort.java
@Component
public class TrendingSort implements SortingStrategy {
public String getName() { return "trending"; }
...
}

// That's it! Spring finds it automatically.

Files changed: 1 (just new class with @Component) Zero configuration, zero if-else, automatic discovery


Comparison Table​

AspectNo InterfaceFactory PatternRegistry PatternSpring Auto-Discovery
If-else location4 files1 factoryStatic blockNone (auto)
Add new strategyUpdate 4 filesUpdate factoryRegister in mapCreate @Component
Risk of inconsistencyHIGHLOWVERY LOWNONE
Configuration neededEverywhereCentralizedOne-time setupZero
TestabilityHardEasyEasyEasy
MaintainabilityPoorGoodBetterBest
Lines changed for new feature40+15105

The Restaurant Analogy​

Without Interface (Scattered If-Else)​

Customer calls ProductListView:
"For pizza call Joe's at 555-0100, for Italian call Maria's at 555-0200..."

Customer calls RecommendationEngine:
"For pizza call Joe's at 555-0100, for Italian call Maria's at 555-0200..."

Customer calls AdminPanel:
"For pizza call Joe's at 555-0100, for Italian call Maria's at 555-0200..."

Customer calls ReportGenerator:
"For pizza call Joe's at 555-0100, for Italian call Maria's at 555-0200..."

Add new restaurant? Tell EVERYONE the new phone number.

With Factory (Centralized If-Else)​

Everyone:
"Call the Restaurant Directory at 555-FOOD"

Restaurant Directory (Factory):
"If you want pizza, call Joe's at 555-0100
If you want Italian, call Maria's at 555-0200
If you want Chinese, call Bob's at 555-0300"

Add new restaurant? Update the directory once. Everyone benefits.

With Registry (No If-Else)​

Restaurant Directory (Registry):
{
"pizza": "Joe's: 555-0100",
"italian": "Maria's: 555-0200",
"chinese": "Bob's: 555-0300"
}

Add new restaurant? Add one entry to the map.

With Spring (Auto-Discovery)​

Restaurants automatically register themselves:

Joe's opens β†’ Adds itself to directory
Maria's opens β†’ Adds itself to directory
Bob's opens β†’ Adds itself to directory
New restaurant opens β†’ Automatically appears in directory

Add new restaurant? Just open it. Automatic discovery!


Real-World Example: Payment Processing​

Without Interface (If-Else Everywhere)​

// CheckoutService.java
if (paymentMethod.equals("creditCard")) {
processCreditCard(order);
} else if (paymentMethod.equals("paypal")) {
processPayPal(order);
}

// SubscriptionService.java
if (paymentMethod.equals("creditCard")) {
processCreditCard(subscription);
} else if (paymentMethod.equals("paypal")) {
processPayPal(subscription);
}

// RefundService.java
if (paymentMethod.equals("creditCard")) {
refundCreditCard(transaction);
} else if (paymentMethod.equals("paypal")) {
refundPayPal(transaction);
}

With Spring Auto-Discovery​

// Interface
public interface PaymentProcessor {
String getName();
PaymentResult process(Order order);
boolean refund(String transactionId);
}

// Implementations (auto-discovered)
@Component
public class CreditCardProcessor implements PaymentProcessor {
public String getName() { return "creditCard"; }
// Implementation...
}

@Component
public class PayPalProcessor implements PaymentProcessor {
public String getName() { return "paypal"; }
// Implementation...
}

@Component
public class StripeProcessor implements PaymentProcessor {
public String getName() { return "stripe"; }
// Implementation...
}

// Registry (auto-populated)
@Service
public class PaymentProcessorRegistry {
private final Map<String, PaymentProcessor> processors;

@Autowired
public PaymentProcessorRegistry(List<PaymentProcessor> allProcessors) {
this.processors = allProcessors.stream()
.collect(Collectors.toMap(
PaymentProcessor::getName,
processor -> processor
));
}

public PaymentProcessor getProcessor(String name) {
return processors.get(name);
}
}

// All services now simple
@Service
public class CheckoutService {
@Autowired
private PaymentProcessorRegistry registry;

public void checkout(Order order, String paymentMethod) {
PaymentProcessor processor = registry.getProcessor(paymentMethod);
processor.process(order);
}
}

Add Crypto payment?

@Component
public class CryptoProcessor implements PaymentProcessor {
public String getName() { return "crypto"; }
// Implementation...
}

Done! No other files change.


Summary​

The Key Insight​

You're absolutely right: If-else doesn't disappear entirely.

But interfaces let you:

Level 0: If-else scattered in 10 files
↓
Level 1: If-else centralized in 1 factory
↓
Level 2: If-else in static registry (map lookup, not conditionals)
↓
Level 3: Zero if-else (Spring auto-discovery)

What You Gain​

BenefitWithout InterfaceWith FactoryWith Spring Auto-Discovery
Update locations10 files1 factory0 (just add @Component)
Risk of bugs10 places1 place0 (automatic)
ConsistencyHard to maintainGuaranteedGuaranteed
TestingHard (mixed concerns)Easy (isolated)Easy (isolated)
New feature effortHIGHMEDIUMLOW

The Pattern​

Scattered conditionals
↓ (interfaces + factory)
Centralized conditionals
↓ (registry pattern)
Mostly eliminated
↓ (Spring auto-discovery)
Fully automated

Final Insight: Interfaces don't eliminate conditionalsβ€”they evolve them from scattered chaos to centralized logic to automatic discovery. That's not moving the problem; that's solving it! 🎯

Tags: #java #design-patterns #factory-pattern #registry-pattern #spring-boot #clean-code