We Were Tired of If-Else Hell…How this Pattern Saved Our Java Microservices
In microservices architecture, extensibility isn’t optional — it’s survival.
We Were Tired of If-Else Hell…How this Pattern Saved Our Java Microservices

In microservices architecture, extensibility isn’t optional — it’s survival.
One fine Monday morning, our payment service broke. Not because of downtime. Not because of scaling.
But because someone added one more payment type.
And that “small change” required modifying:
- 3
if-elseblocks - 2 switch statements
- 4 test classes
- And one forgotten utility file
That’s when we decided: this has to stop.
Enter the Registry Pattern.

The Problem: Conditional Explosion
Imagine a classic e-commerce microservice handling different payment types.
public void processPayment(PaymentRequest request) {
if (request.getType().equals("CREDIT_CARD")) {
creditCardService.process(request);
} else if (request.getType().equals("UPI")) {
upiService.process(request);
} else if (request.getType().equals("NET_BANKING")) {
netBankingService.process(request);
}
}
Looks harmless.
Until product says:
Next sprint we’re adding Wallet, BNPL, Crypto, and International Transfer.
Now your service becomes:
- Hard to extend
- Hard to test
- Violates Open/Closed Principle
- Requires code modification for every new type
This is exactly what we faced in one of our Java microservices running on Spring Boot.
The Idea: Strategy + Registry = Clean Extensibility
The solution?
Use:
- Strategy Pattern → Define behavior per type
- Registry Pattern → Map type → implementation dynamically
No more modifying core logic.
Architecture Before vs After
Before
Controller → If/Else → Concrete Services
After
Controller → Registry → Strategy Implementation
The controller doesn’t care about implementation anymore.
Step 1: Define a Common Strategy Interface
public interface PaymentProcessor {
String getType();
void process(PaymentRequest req
Each implementation declares what type it supports.
Step 2: Concrete Implementations
@Component
public class CreditCardProcessor implements PaymentProcessor {
@Override
public String getType() {
return "CREDIT_CARD";
}
@Override
public void process(PaymentRequest request) {
System.out.println("Processing Credit Card payment");
}
}
@Component
public class UpiProcessor implements PaymentProcessor {
@Override
public String getType() {
return "UPI";
}
@Override
public void process(PaymentRequest request) {
System.out.println("Processing UPI payment");
}
}
Each class owns its logic. No cross-editing.
Step 3: The Registry
This is the magic.
@Component
public class PaymentProcessorRegistry {
private final Map<String, PaymentProcessor> registry = new HashMap<>();
@Autowired
public PaymentProcessorRegistry(List<PaymentProcessor> processors) {
processors.forEach(p ->
registry.put(p.getType(), p)
);
}
public PaymentProcessor getProcessor(String type) {
return Optional.ofNullable(registry.get(type))
.orElseThrow(() ->
new IllegalArgumentException("Unsupported payment type"));
}
}
Spring auto-injects all implementations.
Registry builds the lookup map at startup.
No manual wiring.
Step 4: Clean Service Usage
@Service
public class PaymentService {
private final PaymentProcessorRegistry registry;
public PaymentService(PaymentProcessorRegistry registry) {
this.registry = registry;
}
public void processPayment(PaymentRequest request) {
PaymentProcessor processor =
registry.getProcessor(request.getType());
processor.process(request);
}
}
Now:
- No
if-else - No
switch - No modification required for new types
Just add a new implementation.
Done.
Why This Is Powerful in Microservices
In distributed systems:
- Teams release independently
- Features evolve rapidly
- Business rules change constantly
Registry Pattern ensures:

Real Production Scenario
For example, To use this in an transport re-accommodation microservice (IROP handling).
Based on disruption type:
- Weather
- Crew issue
- Aircraft maintenance
- Airport restriction
Each disruption had its own rebooking strategy.
Instead of:
if (disruption.equals("WEATHER")) ...
We built:
DisruptionStrategyRegistry
When a new regulatory rule was introduced?
We shipped a new strategy class.
Zero change to orchestrator.
Zero regression.
Advanced Version: Enum-Based Strong Typing
Instead of String types:
public enum PaymentType {
CREDIT_CARD,
UPI,
NET_BANKING
}
Registry becomes:
private final Map<PaymentType, PaymentProcessor> registry;
Now you get:
- Compile-time safety
- No typo bugs
- Cleaner contracts
Testing Becomes Beautiful
Unit testing:
@Test
void shouldProcessUpiPayment() {
PaymentProcessor upi = mock(UpiProcessor.class);
when(upi.getType()).thenReturn("UPI");
PaymentProcessorRegistry registry =
new PaymentProcessorRegistry(List.of(upi));
assertNotNull(registry.getProcessor("UPI"));
}
When Should You Use Registry Pattern?
Use it when:
- Behavior varies by type
- New types are expected frequently
- You want strict Open/Closed Principle
- You’re building plug-in style architecture
- Microservice needs dynamic extensibility
Avoid when:
- You only have 2 static types
- Behavior won’t grow
- Simplicity is more important
Final Impact We Observed
After refactoring:
- 40% reduction in merge conflicts
- Faster feature addition
- Cleaner PR reviews
- Lower cognitive load
- Better separation of concerns
Most importantly?
We stopped fearing new payment types.
Closing Thought
In microservices, the real enemy isn’t downtime.
It’s rigidity.
The Registry Pattern quietly transforms your service from:
Edit core logic every sprint
to
Just plug in a new behavior.
And that’s architectural maturity.
메타데이터
- post_id
- 848886e6b02f
- slug
- we-were-tired-of-if-else-hell-how-this-pattern-saved-our-java-microservices-848886e6b02f
- url
- https://medium.com/@letslearnnow/we-were-tired-of-if-else-hell-how-this-pattern-saved-our-java-microservices-848886e6b02f
- canonical_url
- https://medium.com/@letslearnnow/we-were-tired-of-if-else-hell-how-this-pattern-saved-our-java-microservices-848886e6b02f
- author_url
- https://medium.com/@letslearnnow
- status
- ok
- fetched_at
- 2026-06-09 14:34:10