← Back to list

We Were Tired of If-Else Hell…How this Pattern Saved Our Java Microservices

In microservices architecture, extensibility isn’t optional — it’s survival.

Lets Learn Now in Stackademic · 2026-05-25 14:46 · 0 claps · 3.1 min read paywalled
#microservices #java #distributed-systems #design-patterns #software-development
Open on Medium ↗
Wiki topics: 🏛️ · Architecture

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-else blocks
  • 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