บทที่ 17 · Part 4 — Boundaries, Data and Integration
External Payment Providers as Adapters
ออกแบบ PaymentGateway แบบ vendor-neutral และแปล lifecycle, error กับ ambiguous outcomes ที่ Anti-Corruption Layer
External Payment Providers as Adapters
เมื่อ application เรียก Stripe SDK จาก use case โดยตรง PaymentIntent, status และ exception
จะกลายเป็นภาษาหลักของระบบ การเพิ่ม provider ตัวที่ 2 หรือเปลี่ยน API version กระทบ domain
ทั่ว codebase ที่อันตรายกว่าคือ timeout ถูก map เป็น failed ทั้งที่ provider อาจรับคำขอแล้ว
Payment adapter ต้องปกป้องทั้ง language และ uncertainty
จบบทนี้คุณจะ
ออกแบบ provider-independent PaymentGateway, application-owned command/outcome,
translation table และ failure contract ใช้ Stripe documentation เป็น vendor case evidence
พร้อม pin API family/version โดยไม่เขียน complete integration หรืออ้าง vendor behavior เป็นสากล
[!IMPORTANT] วิธีอ่านหลักฐานในบทนี้ คำที่เป็น Definition/Pattern มีแหล่งต้นทางอยู่ใกล้ claim ส่วน decision table, starting structure, checklist และ lab เป็น Heuristic/Trade-off หรือ Course Convention สำหรับฝึกตัดสินใจ ไม่ใช่มาตรฐานสากล เว้นแต่บทจะระบุแหล่งและขอบเขตไว้ชัดเจน
Application เป็นเจ้าของ Port
เริ่มจาก conversation ที่ Checkout ต้องการ:
type PaymentGateway interface {
CreateCharge(context.Context, ChargeRequest) (ChargeOutcome, error)
GetCharge(context.Context, ProviderChargeRef) (ChargeOutcome, error)
}
type ChargeRequest struct {
OperationID string
Amount Money
SourceRef string
}
type ChargeOutcome struct {
Status ChargeOutcomeStatus
ProviderRef string
DeclineCode string
}
type ChargeOutcomeStatus string
const (
OutcomeSucceeded ChargeOutcomeStatus = "SUCCEEDED"
OutcomeDeclined ChargeOutcomeStatus = "DECLINED"
OutcomePending ChargeOutcomeStatus = "PENDING"
OutcomeUnknown ChargeOutcomeStatus = "UNKNOWN"
)
Port ไม่รับ stripe.PaymentIntentParams และไม่คืน provider object OperationID เป็น
business/application idempotency identity ซึ่ง adapter map ไป vendor mechanism ตาม contract
แยก error สำหรับความล้มเหลวในการเรียก capability เช่น invalid local configuration
จาก business/provider outcome เช่น issuer decline Outcome ที่ application ต้อง persist
ไม่ควรหายไปเพราะถูกโยนเป็น generic exception
แปล Model ที่ Anti-Corruption Layer
Adapter มี translation 3 ด้าน:
- Application command → vendor request/metadata/idempotency
- Vendor lifecycle/status → application outcome
- Vendor error/transport result → retryable, terminal หรือ unknown semantics
ตัวอย่าง translation table ต้อง pin API family/version:
| Vendor observation | Application outcome | Next action |
|---|---|---|
| confirmed success ตาม pinned contract | SUCCEEDED | persist fact, publish owned event |
| issuer decline | DECLINED | show safe reason, allow policy-controlled retry |
| asynchronous state | PENDING | wait/query/webhook |
| validation rejected before operation | terminal error | fix request; no blind retry |
| timeout/connection loss after send | UNKNOWN | query by stable reference/reconcile |
| server error with uncertain effect | UNKNOWN | follow vendor guidance; no new operation ID |
ตารางนี้ต้องสร้างจาก provider docs/version ที่ใช้จริง ไม่ copy ระหว่าง vendors
กรณี Stripe เป็นหลักฐานเชิงปฏิบัติ
Case pin สำหรับบทนี้
ตัวอย่างใช้ Stripe API v1 PaymentIntent, API version 2026-07-29.dahlia
และตรวจเอกสารทางการล่าสุดเมื่อ 2026-08-08 Go adapter ต้องเลือก stripe-go release
ที่ stripe.APIVersion ตรงกับ version นี้ และสร้าง webhook endpoint ด้วย version เดียวกัน
Stripe versioning for Go
อธิบายว่า strongly typed stripe-go ผูก request กับ API version ของ SDK release และแนะนำ
ให้ webhook endpoint ใช้ version เดียวกัน หากเปลี่ยน version ต้อง review changelog,
translation table และ contract tests ใหม่ทั้งหมด
Stripe idempotent requests อธิบายการส่ง idempotency key กับ mutation และการใช้ parameters เดิมเมื่อ retry ส่วน low-level errors อธิบาย network/server uncertainty ที่ต้องระวัง ตัวอย่างภายใต้ pin ข้างต้นจึงเก็บ operation ID ก่อน call และใช้ key เดิมสำหรับ logical operation
PaymentIntent เป็น vendor object ที่ช่วย ติดตาม payment lifecycle แต่ application ไม่ควรเท่ากับ status ทุกตัวตรง ๆ Adapter pin version ใน config/test fixture และ review changelog ก่อน upgrade
Timeout ไม่ได้แปลว่าเงินไม่เคลื่อน
หาก connection ขาดหลัง provider รับคำขอ client อาจไม่รู้ผล ห้ามสร้าง operation ใหม่
ด้วย idempotency key ใหม่หรือคืน FAILED ทันที ให้ persist UNKNOWN, query ด้วย stable
reference และ reconcile ตาม provider contract เพื่อป้องกัน double charge
Error Contract และ Observability
Application ควรแยกอย่างน้อย:
- invalid request ซึ่ง retry เดิมไม่ช่วย
- decline ซึ่งเป็น payment outcome ไม่ใช่ system outage
- provider unavailable/retryable ก่อนรู้ว่า request ถูกยอมรับหรือไม่
- unknown effect ซึ่งต้องสอบถาม
- pending async lifecycle
- authentication/configuration failure ซึ่งต้อง alert operator
Log provider request ID, internal operation ID, API version และ normalized outcome โดยไม่ log PAN, secret, token หรือ sensitive payload Metric ควรบอก latency, outcome class, unknown count, reconciliation age และ idempotency conflict
Adapter ไม่ควรตัดสิน business retry limit หรือ refund eligibility มันรายงาน normalized fact ให้ application/domain owner ตัดสิน
ขอบเขตของ Folder
internal/modules/payment/
ports.go
create.go
outcome.go
internal/adapters/paymentgateway/stripe/
client.go
translate.go
webhook.go
contract_test.go
stripe package import payment port types ส่วน payment module ไม่ import Stripe SDK
contract tests ใช้ official sandbox/fixtures ตาม version และ unit tests ตรวจ translation
ของทุก status/error ที่ application อาศัย
แบบฝึกปฏิบัติ: ออกแบบ Boundary โดยยังไม่เขียน Vendor Adapter
ส่งมอบ:
- Go port/command/outcome ที่ไม่มี vendor types
- translation table พร้อม version/source link
- idempotency identity และ retry rule
- unknown-state recovery sequence
- safe logs/metrics fields
- folder/import rules
ทดลอง 3 scenario: decline, timeout หลัง send และ webhook success ก่อน synchronous response หาก contract ต้องเดาจาก HTTP status อย่างเดียว ให้กลับไปอ่าน vendor lifecycle
รายการตรวจสอบ
- Port ใช้ application language และ consumer-owned types
- Amount เป็น integer minor units + currency
- Vendor API family/version ถูก pin และอยู่ใน contract tests
- Decline, pending, unavailable และ unknown แยกกัน
- Retry logical operation ใช้ stable identity ตาม vendor contract
- Adapter ไม่เป็นเจ้าของ refund/routing business policy โดยไม่ได้รับมอบหมาย
- Logs ไม่มี secrets/payment credentials
สรุปบทนี้
Payment adapter เป็น Anti-Corruption Layer ที่แปลทั้งข้อมูล lifecycle และ uncertainty
PaymentGateway ไม่ได้ทำให้ vendor เหมือนกันทุกเจ้า แต่ทำให้ application เป็นเจ้าของ
คำถามของตน Stripe docs เป็น evidence สำหรับ adapter นั้น ไม่ใช่ universal protocol
อ่านเพิ่มเติม
- Stripe PaymentIntents และ lifecycle
- Stripe idempotent requests
- Stripe low-level errors
- Hexagonal Architecture และ DDD Reference — Port/Adapter และ ACL