บทที่ 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 ต้องออกแบบแยกกัน

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