01
Multi-Vendor Payment System and Arrears Recovery
Owned the service from initial design to implementation
Owned the payment service from initial design through implementation, expanding a single payment flow into a modular structure that supports multiple payment vendors and building DLQ-based unpaid-event processing with automatic recovery. Later hardened operational paths such as DLQ retry stuck prevention, PG not-found repayment continuity, and cancellation-message branching.
Design Context
The service needed to move beyond a single-payment structure into one that can absorb multiple payment vendors through the same extension point, while also handling unpaid-processing flows.
Key Point
A project for explaining both PG expansion and unpaid-event retry strategy.
Core Implementation
- Built a PG integration architecture with an abstract-class-based vendor strategy pattern.
- Implemented a GCP Pub/Sub-based DLQ pattern that isolates failed events and keeps arrears-processing targets traceable.
- Implemented retries, DLQ, and NACK-based recovery paths so unpaid events could flow into follow-up payment recovery.
- Hardened FAILOVER transitions and retry timestamp recording to prevent dead-letter retries from getting stuck, while keeping unpaid-payment recovery flows intact on PG not-found responses.
- Separated AlimTalk contexts for full, partial, and roaming payment cancellations and normalized amount formatting to improve user-facing communication accuracy.
Engineering Lens
- Centered request orchestration, point hold/confirm, and success/failure transitions around PaymentProcessor so state changes remain traceable in one place.
- Protected point holds from concurrent payment attempts by keeping both hold and payment-READY creation inside a user-scoped lock.
- Split PG-network uncertainty into repair, consumer retries, and failover batches so immediate recovery and operator intervention remain clearly separated.
- Treated retries themselves as observable operations, leaving latestRetriedAt and state transitions behind so operators can tell where a failed event stopped.
Architecture Snapshot
Mermaid View
Multi-vendor orchestration: lock, hold, and state transitions
Keeps request data, lock key, point hold, payment READY creation, internal `PG Vendor` approval steps, and success/failure transitions in one integrated diagram.
flowchart TD
Req["pay / rePay<br/>order=ORD-240915-001<br/>user=421 method=17 point=2000"] --> Lock["distributed lock<br/>payment-user-process:421"]
Lock --> Hold["PointUpdater.hold()<br/>wallet -> HOLD 2000P"]
Hold --> Ready["createWithReady()<br/>payment READY"]
Ready --> Sub["resolve subscription<br/>methodId=17 or primary"]
Sub --> Vendor["PG Vendor Router<br/>supports: KakaoPay / TossPayments / KakaoT<br/>selected: KakaoT"]
subgraph PGV["PG Vendor Layer"]
direction TD
Vendor --> Keys["read vendor keys<br/>pgPayKey + token"]
Keys --> Api["vendor client.pay(...)"]
Api --> Tx["save pgTransactionId<br/>paymentId / tid / paymentKey"]
end
Tx --> Result{"approval result"}
Result -->|success| Done["updateSuccess<br/>payment PAID<br/>point HOLD->CONFIRM"]
Result -->|fail| Fail["updateFailed<br/>releaseHold(order)"]
Fail --> Recovery["repair / retry / failover"]
classDef vendor fill:#dff2ff,stroke:#0f4c81,stroke-width:2px,color:#0f172a;
class Vendor,Keys,Api,Tx vendor;
style PGV fill:#eef7fb,stroke:#0f4c81,stroke-width:2px,color:#0f172a; Mermaid View
Multi-vendor system: mandatory contracts and optional extensions
Places `VendorChecker.select()` on top of the `VendorType` extension point, separates contracts required for every vendor from features needed by only some vendors, and uses `@RequiredVendor` plus `VendorRequirementsValidator` to catch missing mandatory implementations at startup.
flowchart TD
Vendors["VendorType<br/>KakaoPay / TossPayments / KakaoT"] --> Select["VendorChecker.select(vendorType)"]
Select --> Required["Required on all vendors<br/>VendorPaymentProcessor<br/>VendorMethodProcessor"]
Select --> Partial["Required on some vendors<br/>VendorPaymentOnceProcessor<br/>(KakaoPay only)"]
Select --> Optional["Optional extensions<br/>RepairService / vendor hooks"]
Required --> Validate["@RequiredVendor<br/>+ VendorRequirementsValidator"]
Partial --> Validate
Validate --> Boot{"startup validation"}
Boot -->|missing| Error["application start fail"]
Boot -->|ok| Route["route to concrete impl"]
classDef core fill:#dff2ff,stroke:#0f4c81,stroke-width:2px,color:#0f172a;
classDef optional fill:#edf9f3,stroke:#2f6f57,stroke-width:2px,color:#0f172a;
classDef error fill:#fff1f2,stroke:#be123c,stroke-width:2px,color:#0f172a;
class Vendors,Select,Required,Partial,Validate,Route core;
class Optional optional;
class Error error; Operational Outcomes
- Built a payment architecture that integrates multiple PG vendors behind one interface and remains extensible as new vendors are added.
- Applied DLQ-based unpaid-event processing and automatic recovery paths in operation.
- Improved trust in unpaid-payment recovery and user-facing cancellation messages by hardening external PG exception handling and cancellation-message branching.