บทที่ 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 ด้าน:

  1. Application command → vendor request/metadata/idempotency
  2. Vendor lifecycle/status → application outcome
  3. Vendor error/transport result → retryable, terminal หรือ unknown semantics

ตัวอย่าง translation table ต้อง pin API family/version:

Vendor observationApplication outcomeNext action
confirmed success ตาม pinned contractSUCCEEDEDpersist fact, publish owned event
issuer declineDECLINEDshow safe reason, allow policy-controlled retry
asynchronous statePENDINGwait/query/webhook
validation rejected before operationterminal errorfix request; no blind retry
timeout/connection loss after sendUNKNOWNquery by stable reference/reconcile
server error with uncertain effectUNKNOWNfollow 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

ส่งมอบ:

  1. Go port/command/outcome ที่ไม่มี vendor types
  2. translation table พร้อม version/source link
  3. idempotency identity และ retry rule
  4. unknown-state recovery sequence
  5. safe logs/metrics fields
  6. 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

อ่านเพิ่มเติม