Factory Pattern: From Scattered If-Else Hell to Zero Conditionals
"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:
- Spring scans for all
@Componentclasses - Finds all classes implementing
SortingStrategy - Injects them all into
SortingStrategyRegistryconstructor - Registry builds map from strategy names
- New strategy? Just create
@Componentclass - done!
The Evolution: Side-by-Side Comparisonβ
Adding "Trending" Sortβ
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β
| Aspect | No Interface | Factory Pattern | Registry Pattern | Spring Auto-Discovery |
|---|---|---|---|---|
| If-else location | 4 files | 1 factory | Static block | None (auto) |
| Add new strategy | Update 4 files | Update factory | Register in map | Create @Component |
| Risk of inconsistency | HIGH | LOW | VERY LOW | NONE |
| Configuration needed | Everywhere | Centralized | One-time setup | Zero |
| Testability | Hard | Easy | Easy | Easy |
| Maintainability | Poor | Good | Better | Best |
| Lines changed for new feature | 40+ | 15 | 10 | 5 |
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β
| Benefit | Without Interface | With Factory | With Spring Auto-Discovery |
|---|---|---|---|
| Update locations | 10 files | 1 factory | 0 (just add @Component) |
| Risk of bugs | 10 places | 1 place | 0 (automatic) |
| Consistency | Hard to maintain | Guaranteed | Guaranteed |
| Testing | Hard (mixed concerns) | Easy (isolated) | Easy (isolated) |
| New feature effort | HIGH | MEDIUM | LOW |
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
