OOPS Interview Questions · Question 22

A payment application supports Card, UPI, and Wallet payments. How would you design it so new payment methods can be added easily?

Interview preparation resource from Gate Smashers.

Interview-ready answer

Design the payment module around a common PaymentProvider interface and a PaymentService that delegates to the appropriate provider through a registry or factory. Card, UPI, and Wallet are separate implementations of the same contract. To add a new method, implement the interface, register it under a method key, and provide any adapter needed for an external gateway SDK. The orchestration code remains unchanged, following the Open/Closed Principle.

Java example: interface, registry, and orchestratorjava
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

public final class PaymentRequest {
    public final String idempotencyKey;
    public final String method;
    public final long amount;

    public PaymentRequest(String idempotencyKey, String method, long amount) {
        this.idempotencyKey = idempotencyKey;
        this.method = method;
        this.amount = amount;
    }
}

public final class PaymentResponse {
    public final boolean success;
    public final String providerReference;
    public final String errorCode;

    public PaymentResponse(boolean success, String providerReference, String errorCode) {
        this.success = success;
        this.providerReference = providerReference;
        this.errorCode = errorCode;
    }
}

public interface PaymentProvider {
    String key();
    PaymentResponse process(PaymentRequest request);
}

public final class ProviderRegistry {
    private final Map<String, PaymentProvider> providers = new ConcurrentHashMap<>();

    public void register(PaymentProvider provider) {
        providers.put(normalize(provider.key()), provider);
    }

    public Optional<PaymentProvider> lookup(String key) {
        return Optional.ofNullable(providers.get(normalize(key)));
    }

    private String normalize(String key) {
        return key.toUpperCase(Locale.ROOT);
    }
}

public final class PaymentService {
    private final ProviderRegistry registry;

    public PaymentService(ProviderRegistry registry) {
        this.registry = registry;
    }

    public PaymentResponse pay(PaymentRequest request) {
        if (request == null || request.method == null || request.amount <= 0) {
            return new PaymentResponse(false, null, "invalid_request");
        }

        return registry.lookup(request.method)
                .map(provider -> provider.process(request))
                .orElseGet(() -> new PaymentResponse(false, null, "unsupported_method"));
    }
}

public final class CardProvider implements PaymentProvider {
    @Override
    public String key() {
        return "CARD";
    }

    @Override
    public PaymentResponse process(PaymentRequest request) {
        // Call a card gateway through an adapter and map its result.
        return new PaymentResponse(true, "gateway-reference", null);
    }
}

// Application wiring
ProviderRegistry registry = new ProviderRegistry();
registry.register(new CardProvider());
registry.register(new UpiProvider());
registry.register(new WalletProvider());

PaymentService service = new PaymentService(registry);
PaymentResponse response = service.pay(
        new PaymentRequest("idem-1", "CARD", 1000)
);
Understand it clearly

Core design

Use the Strategy pattern to represent each payment method behind a common interface. PaymentService should contain shared workflow concerns, such as request validation, provider selection, idempotency handling, and normalized error handling, while individual providers contain method-specific gateway logic.

A registry or factory maps a payment method key, such as CARD, UPI, or WALLET, to its PaymentProvider implementation. This prevents PaymentService from depending directly on concrete provider classes.

  • PaymentProvider: Defines the common operation for processing a payment.
  • PaymentService: Coordinates the payment flow and delegates to the selected provider.
  • ProviderRegistry: Stores and resolves providers by payment-method key.
  • Adapter: Wraps a third-party payment SDK and translates between application models and SDK-specific models.

Adding a payment method

A new payment method should be introduced without modifying PaymentService or existing providers. Create a new PaymentProvider implementation, adapt its external gateway if necessary, and register it with the registry during application configuration or dependency-injection setup.

  • Implement: Create a provider that implements the common PaymentProvider contract.
  • Adapt: Keep gateway-specific request conversion, response conversion, and error translation inside the provider or an adapter.
  • Register: Associate the provider with a unique payment-method key so it can be resolved at runtime.
  • Verify: Test that the provider follows the common contract, including success, failure, timeout, and idempotency behavior where supported.

Operational considerations

Keep provider-specific details out of the public response model so callers receive a stable application-level result. Store credentials securely, avoid logging sensitive payment data, and add provider-level monitoring.

Timeouts and carefully controlled retries are important for external gateways. Retries must be compatible with idempotency so that a transient failure does not unintentionally create duplicate charges.

  • Security: Protect credentials and avoid exposing sensitive payment information in logs or errors.
  • Resilience: Apply timeouts and use idempotency-aware retry behavior for gateway calls.
  • Observability: Record provider-level latency, outcomes, and structured diagnostic information.