บทที่ 20 · Part 4 — Boundaries, Data and Integration
HTTP, Webhook and Messaging Adapters
แปลง transport contracts เป็น application commands พร้อม authenticity, deduplication และ out-of-order handling
HTTP, Webhook and Messaging Adapters
Provider webhook ส่ง event ซ้ำและมาไม่เรียงลำดับ Worker deserialize vendor JSON แล้วเขียน Charge status โดยตรง ผลคือ event เก่าทับ state ใหม่และ provider field กระจายเข้าทุก layer HTTP, webhook และ message consumer เป็น driving adapters: ต้องพิสูจน์ caller/message, แปล contract และเรียก application use case ไม่ใช่เป็นเจ้าของ domain state
จบบทนี้คุณจะ
แยก transport validation จาก business validation ออกแบบ HTTP/error mapping และ webhook adapter ที่ verify authenticity, deduplicate, tolerate out-of-order และตัดสินว่า event เป็น authoritative fact หรือเพียง hint สำหรับ trusted re-read
[!IMPORTANT] วิธีอ่านหลักฐานในบทนี้ คำที่เป็น Definition/Pattern มีแหล่งต้นทางอยู่ใกล้ claim ส่วน decision table, starting structure, checklist และ lab เป็น Heuristic/Trade-off หรือ Course Convention สำหรับฝึกตัดสินใจ ไม่ใช่มาตรฐานสากล เว้นแต่บทจะระบุแหล่งและขอบเขตไว้ชัดเจน
หน้าที่ของ Driving Adapter
Adapter รับผิดชอบ:
- parse/size/content-type/schema validation
- authenticate caller/sender ตาม protocol
- extract correlation/idempotency metadata
- map external DTO เป็น application command
- call use case และ map application outcome เป็น transport response/ack
- emit safe observability
Adapter ไม่ควรตัดสิน refundable amount, account eligibility หรือ invoice lifecycle
Basic syntax เช่น field missing อยู่ transport ส่วน Charge cannot be refunded in this state
อยู่ owning domain
ขอบเขต HTTP
type refundRequest struct {
AmountMinor int64 `json:"amount_minor"`
Currency string `json:"currency"`
}
func (h Handler) ApproveRefund(w http.ResponseWriter, r *http.Request) {
principal := PrincipalFromContext(r.Context())
var body refundRequest
if err := decodeLimitedJSON(r, &body); err != nil {
writeProblem(w, http.StatusBadRequest, "invalid_request")
return
}
cmd := ApproveRefund{RefundID: PathRefundID(r), AmountMinor: body.AmountMinor, Currency: body.Currency}
result, err := h.approve(r.Context(), principal, cmd)
writeRefundOutcome(w, result, err)
}
Map error อย่างตั้งใจ: malformed input 400, unauthenticated 401 ตาม protocol, forbidden 403, not found ตาม disclosure policy, conflict สำหรับ version/state conflict และ 5xx สำหรับ unavailable อย่า expose stack/SQL/vendor error
ตรวจความแท้จริงของ Webhook และป้องกัน Replay
Stripe webhook guidance ระบุ signature verification, duplicate handling และ event ordering caveats ตรวจ signature บน raw payload ก่อน parse ตาม official library/secret rotation contract และจำกัด timestamp tolerance ตาม policy
flow:
receive raw bytes → size limit → verify signature/authenticity
→ parse pinned schema → persist delivery ID/inbox atomically
→ acknowledge according to provider contract → process asynchronously
→ translate to application fact/hint → update via owning use case
Unique inbox key เช่น (provider, account, event_id) ป้องกัน duplicate delivery แต่ business
effect ยังควร idempotent ด้วย operation/reference เพราะ provider อาจส่ง semantic duplicate
ต่าง event IDs
Authentic webhook ยังไม่เท่ากับ authoritative decision
Signature บอกว่าข้อความมาจากผู้ถือ secret/key ไม่ได้บอกว่า event ใหม่กว่าสถานะปัจจุบัน หรือเพียงพออนุมัติ financial effect Adapter ต้องตรวจ resource/account binding, ordering/version และอาจ query provider ด้วย trusted credential ก่อนเปลี่ยน state
Event เป็นข้อเท็จจริงหรือเพียงสัญญาณ
เลือกหนึ่งใน 2 contract:
Authoritative external fact — provider contract รับรอง event/object/version ที่ใช้ได้
Adapter translate เป็น ProviderPaymentConfirmed พร้อม dedupe/order policy
Hint to re-read — event บอกว่า object เปลี่ยน Application query provider API ด้วย stable reference แล้วใช้ latest normalized outcome วิธีนี้ลด dependence on event ordering แต่เพิ่ม API availability/rate-limit concerns
บันทึก decision ต่อ event type ไม่ใช้ policy เดียวทุก provider
Adapter สำหรับ Messaging
Internal message consumer ต้อง validate envelope/schema/version, tenant/source, correlation, idempotency และ ownership เหมือน webhook อย่า deserialize public event เข้า Aggregate แล้ว save โดยตรง ให้ map เป็น application command/fact
Ack semantics ต้องสอดคล้อง broker: acknowledge หลัง durable inbox/local effect หรือออกแบบ redelivery path หาก process crash อย่าเคลม exactly-once จาก broker อย่างเดียว
Out-of-order strategy:
- compare source version/sequence ถ้า contract มี
- accept only legal monotonic transition ใน domain
- query owner for current state
- park/dead-letter พร้อม operator workflow แทน drop เงียบ
แบบฝึกปฏิบัติ: Payment Event Adapter
ออกแบบ event handler สำหรับ provider payment update พร้อม artifacts:
Authentication/signature method:
Raw payload constraints:
Pinned schema/API version:
Inbox uniqueness:
Vendor-to-application translation:
Fact vs hint decision:
Out-of-order behavior:
Ack/retry/dead-letter behavior:
Safe audit/metrics:
ทดสอบ invalid signature, duplicate ID, semantic duplicate, old event หลัง success, unknown event type, provider query unavailable และ crash หลัง inbox insert
รายการตรวจสอบ
- Transport adapter มี parse/auth/map/call responsibilities ชัด
- Business rules อยู่ application/domain owner
- Webhook signature ตรวจ raw body ด้วย official contract
- Delivery dedupe แยกจาก business idempotency
- Out-of-order ไม่ทับ legal state ย้อนหลัง
- Fact vs hint ถูกบันทึกต่อ event type
- Ack/retry สัมพันธ์กับ durable processing point
- External errors/payload ไม่รั่วเข้า public/domain contract
สรุปบทนี้
Driving adapter คือประตูล่ามและยาม ไม่ใช่ domain owner HTTP, webhook และ message ต่างกันด้าน delivery/authentication แต่ทุกแบบต้อง map เป็น application language Authenticity, deduplication, ordering และ authoritative semantics ต้องออกแบบแยกกัน
อ่านเพิ่มเติม
- Stripe Webhooks — official delivery/signature guidance
- Idempotent Receiver — messaging pattern
- Hexagonal Architecture — driving adapters
- OWASP REST Security Cheat Sheet — HTTP API boundary controls