Back to Home

Code Showcase

Production-quality code patterns from my 9 years of experience

Processor Pattern

Java 8Design Pattern

#Strategy#Factory#Payment
// Interface defining contract for all payment processors
public interface PaymentProcessor {
    PaymentMethod getMethod();
    boolean supports(PaymentRequest request);
    Either<PaymentError, PaymentResult> process(PaymentRequest request);
    default int getPriority() { return 0; }
}

// Abstract class providing template method
public abstract class AbstractPaymentProcessor implements PaymentProcessor {

    @Override
    public Either<PaymentError, PaymentResult> process(PaymentRequest request) {
        return validateRequest(request)
            .flatMap(this::buildProviderRequest)
            .flatMap(this::callProvider)
            .flatMap(this::parseResponse)
            .peek(result -> logTransaction(request, result));
    }

    // Template methods - subclass implement
    protected abstract Either<PaymentError, ProviderRequest> buildProviderRequest(PaymentRequest req);
    protected abstract Either<PaymentError, ProviderResponse> callProvider(ProviderRequest req);
    protected abstract Either<PaymentError, PaymentResult> parseResponse(ProviderResponse resp);
}

// Concrete implementation - MoMo
@Component
@RequiredArgsConstructor
public class MoMoProcessor extends AbstractPaymentProcessor {
    private final MoMoClient momoClient;
    private final SignatureService signatureService;

    @Override
    public PaymentMethod getMethod() {
        return PaymentMethod.MOMO;
    }

    @Override
    protected Either<PaymentError, ProviderRequest> buildProviderRequest(PaymentRequest req) {
        var momoReq = MoMoRequest.builder()
            .partnerCode(config.getPartnerCode())
            .orderId(req.getOrderId())
            .amount(req.getAmount())
            .signature(signatureService.sign(req))
            .build();
        return Either.right(momoReq);
    }

    @Override
    protected Either<PaymentError, ProviderResponse> callProvider(ProviderRequest req) {
        try {
            return Either.right(momoClient.createPayment((MoMoRequest) req));
        } catch (Exception e) {
            return Either.left(new ProviderConnectionError(e));
        }
    }
}

// Registry auto-collects all processors via DI
@Component
public class ProcessorRegistry {
    private final List<PaymentProcessor> processors;

    public ProcessorRegistry(List<PaymentProcessor> processors) {
        this.processors = processors.stream()
            .sorted(Comparator.comparing(PaymentProcessor::getPriority).reversed())
            .collect(Collectors.toList());
    }

    public Either<PaymentError, PaymentResult> route(PaymentRequest request) {
        return processors.stream()
            .filter(p -> p.supports(request))
            .findFirst()
            .map(p -> p.process(request))
            .orElse(Either.left(new NoProcessorFoundError(request.getMethod())));
    }
}

Key Points

  • 1Interface + Abstract class: Define contract and template method
  • 2Either monad: Functional error handling, no thrown exceptions
  • 3Auto-discovery: Spring auto-injects all implementations
  • 4Priority routing: Processors sorted by priority for selection
  • 5Adding new provider: Just create new class implementing interface