
はじめに
実際の決済プラットフォームでは、成功率、コスト、適用範囲を最適化するために、常に複数の PSP (決済サービス プロバイダー) を統合する必要があります。この記事では、スマート ルーティングを使用したマルチ PSP 統合のための抽象化レイヤーの設計について説明します。
1. PSP 抽象化レイヤー
1.1 アダプターのパターン
┌────────────────────────────────────────────────────┐
│ PAYMENT SERVICE │
│ │
│ ┌──────────────────────────────────────────────┐ │
│ │ PSP Interface (Port) │ │
│ │ authorize() / capture() / refund() / void() │ │
│ └──────────────────┬───────────────────────────┘ │
│ │ │
│ ┌──────────────┼──────────────┐ │
│ │ │ │ │
│ ┌───▼────┐ ┌─────▼────┐ ┌─────▼────┐ │
│ │ VNPay │ │ MoMo │ │ Stripe │ ... │
│ │Adapter │ │ Adapter │ │ Adapter │ │
│ └───┬────┘ └─────┬────┘ └─────┬────┘ │
└──────┼──────────────┼─────────────┼────────────────┘
│ │ │
┌───▼────┐ ┌─────▼────┐ ┌────▼─────┐
│ VNPay │ │ MoMo │ │ Stripe │
│ API │ │ API │ │ API │
└────────┘ └──────────┘ └──────────┘
1.2 PSP インターフェース
public interface PaymentServiceProvider {
String getId();
Set<PaymentMethod> supportedMethods();
Set<Currency> supportedCurrencies();
AuthResult authorize(AuthRequest request);
CaptureResult capture(CaptureRequest request);
RefundResult refund(RefundRequest request);
VoidResult voidPayment(VoidRequest request);
PaymentStatus queryStatus(String pspReference);
}
1.3 VNPay アダプター
@Component
public class VNPayAdapter implements PaymentServiceProvider {
@Override
public String getId() { return "vnpay"; }
@Override
public Set<PaymentMethod> supportedMethods() {
return Set.of(
PaymentMethod.ATM_CARD,
PaymentMethod.CREDIT_CARD,
PaymentMethod.QR_CODE,
PaymentMethod.BANK_TRANSFER
);
}
@Override
public AuthResult authorize(AuthRequest request) {
// Build VNPay-specific parameters
var params = new TreeMap<String, String>();
params.put("vnp_Version", "2.1.0");
params.put("vnp_Command", "pay");
params.put("vnp_TmnCode", config.getTmnCode());
params.put("vnp_Amount", String.valueOf(
request.getAmount().multiply(100).longValue()));
params.put("vnp_CurrCode", "VND");
params.put("vnp_TxnRef", request.getOrderId());
params.put("vnp_OrderInfo", request.getDescription());
params.put("vnp_ReturnUrl", config.getReturnUrl());
// Sign with HMAC-SHA512
String signature = HmacUtils.hmacSha512(
config.getHashSecret(), buildQueryString(params));
params.put("vnp_SecureHash", signature);
// Return redirect URL for customer
String paymentUrl = config.getPayUrl() + "?" + buildQueryString(params);
return AuthResult.redirect(paymentUrl);
}
}
2. スマートルーティング
2.1 ルーティング エンジン
@Service
public class PSPRouter {
private final List<RoutingRule> rules;
private final PSPHealthMonitor healthMonitor;
public PaymentServiceProvider selectPSP(PaymentRequest request) {
// 1. Filter by capability
var candidates = pspRegistry.getAll().stream()
.filter(psp -> psp.supportedMethods().contains(request.getMethod()))
.filter(psp -> psp.supportedCurrencies().contains(request.getCurrency()))
.filter(psp -> healthMonitor.isHealthy(psp.getId()))
.toList();
// 2. Apply routing rules
for (var rule : rules) {
var result = rule.evaluate(request, candidates);
if (result.isPresent()) return result.get();
}
// 3. Default: cost-optimized selection
return candidates.stream()
.min(Comparator.comparing(psp -> getFeeRate(psp, request)))
.orElseThrow(() -> new NoPSPAvailableException());
}
}
2.2 ルーティング戦略
| 戦略 | ロジック | 使用例 |
|---|---|---|
| コストの最適化 | 最低料金のPSP | デフォルト |
| 成功率 | 最高の成功率 PSP | 高額取引 |
| レイテンシ最適化 | 最速のPSP | リアルタイム支払い |
| 地理 | お客様に一番近いPSP | 国境を越えて |
| 負荷分散 | ラウンドロビン/加重 | トラフィック分散 |
| A/B テスト | ランダム分割 | 新しい PSP のテスト |
2.3 フェイルオーバー パターン
Primary PSP (VNPay)
│
├── Success → Done
│
├── Timeout/5xx → Retry once
│ │
│ ├── Success → Done
│ │
│ └── Fail → Fallback PSP (Stripe)
│ │
│ ├── Success → Done
│ │
│ └── Fail → Return error to customer
│
└── Terminal error (declined) → Return error (no fallback)
3. PSP ヘルスモニタリング
@Component
public class PSPHealthMonitor {
// Track success rate per PSP with sliding window
private final Map<String, SlidingWindowCounter> successRates;
@Scheduled(fixedRate = 60000) // Every minute
public void checkHealth() {
for (var entry : successRates.entrySet()) {
var pspId = entry.getKey();
var counter = entry.getValue();
double rate = counter.getSuccessRate();
if (rate < 0.5) { // < 50% success rate
circuitBreaker.open(pspId);
alertService.send("PSP " + pspId + " degraded: " + rate);
}
}
}
public boolean isHealthy(String pspId) {
return !circuitBreaker.isOpen(pspId);
}
}
4. Webhook の処理
4.1 PSP コールバックの受信
@RestController
@RequestMapping("/webhooks")
public class WebhookController {
@PostMapping("/vnpay")
public ResponseEntity<String> handleVNPayCallback(
@RequestParam Map<String, String> params) {
// 1. Verify signature
if (!vnpayAdapter.verifySignature(params)) {
return ResponseEntity.badRequest().body("Invalid signature");
}
// 2. Process callback
var paymentId = params.get("vnp_TxnRef");
var responseCode = params.get("vnp_ResponseCode");
webhookProcessor.process(WebhookEvent.builder()
.pspId("vnpay")
.paymentId(paymentId)
.status(mapStatus(responseCode))
.rawPayload(params)
.build());
return ResponseEntity.ok("OK");
}
@PostMapping("/stripe")
public ResponseEntity<Void> handleStripeWebhook(
@RequestBody String payload,
@RequestHeader("Stripe-Signature") String signature) {
// Verify Stripe webhook signature
var event = stripeAdapter.verifyAndParse(payload, signature);
webhookProcessor.process(event);
return ResponseEntity.ok().build();
}
}
概要
マルチ PSP 統合には以下が必要です。
- PSP 固有の API を抽象化するための アダプター パターン
- スマート ルーティング により、コスト、成功率、遅延を最適化します。
- PSP がクラッシュすると自動的に フェイルオーバー
- ヘルスモニタリング、サーキットブレーカー付き
- 署名検証による各 PSP の Webhook 処理
次の記事: 調整および決済エンジン — 制御システムと自動決済プロセス。