Lego ↔ Orchestration — Master Design
Master design การ integrate Lego Engine (UserService) ↔ Orchestrator ↔ Workflow ↔ Task — Approach A: Saga-Projected Single Status (saga ProcessState = source of truth ช่วง in-flight, Lego mirror)
อัปเดต: 2026-06-15
โจทย์: Orchestration Service ต้องเป็น control plane ที่คุม flow ระหว่าง Lego Engine (UserService) ↔ Workflow ↔ Task ไม่ใช่แค่ “bridge” — และต้องไม่ให้ business logic ของ status progression ฝังซ้ำ 2 service ผลลัพธ์: Approach A — Saga-Projected Single Status (saga
ProcessState= source of truth ของช่วง in-flight, Lego mirror) ขอบเขต: 4 service — UserService(Lego) / OrchestratorService / WorkflowService / TaskService Reconciled กับ grill 2026-06-15: ดู CONTEXT.md — 4 conflict + scope resolved ฐานข้อมูลเดิม:09062026/(ORCHESTRATION_SERVICE_DESIGN + Lane1/Lane2 + ADR 0031-0033),SA-1557-GAP-ANALYSIS
เอกสารชุดนี้
| ไฟล์ | สำหรับ |
|---|---|
| 00-OVERVIEW.md (ไฟล์นี้) | ภาพรวม + sequence diagrams + contract matrix + metrics — อ่านก่อน |
| CONTEXT.md | glossary + decisions ของ grill (อ่านคู่กัน) |
| 01-userservice-lego.md | Dev ฝั่ง Lego (UserService) |
| 02-orchestrator.md | Dev ฝั่ง Orchestrator (Lane 2 + saga) |
| 03-workflowservice.md | Dev ฝั่ง Workflow (contract = ต้องคุยกับทีม) |
| 04-taskservice.md | Dev ฝั่ง Task (contract = ต้องคุยกับทีม) |
Scope (grill 2026-06-15): เอกสารชุดนี้ออกแบบ integration contract (จุดที่ service คุยกัน) ส่วน Workflow/Task internals = นอกขอบเขต — contract ของ 2 service นั้น (M3-M5, M9-M14) เป็น ข้อเสนอไปคุยกับทีมเจ้าของ ไม่ใช่ค่าที่ lock ฝ่ายเดียว
1. หลักการ (Approach A)
┌──────────────────────────────────────────────┐
│ Orchestrator Saga (ProcessState) │
│ = SOURCE OF TRUTH ของช่วง in-flight │
│ (ไม่ทำ notification — §ownership) │
└──────────────────────────────────────────────┘
▲ events │ projection + commands
│ ▼
┌─────────────┐ ASB/HTTP ┌─────────────┐
│ Lego │◀─────────────────▶│ Workflow │
│ (UserService)│ (ผ่าน Orch) │ + Task │
│ = MIRROR │ │ = ผู้ลงมือ │
│ notify→requestor │ notify→maker│
└─────────────┘ └─────────────┘
Lego เป็นเจ้าของเองเฉพาะ: Draft (ก่อน submit) + Resubmitted (กด resubmit) + automation chain หลัง Approved → Finalized
ช่วง in-flight (InReview/Reviewed) Lego แค่ mirror ตามที่ saga project ลงมา
ทำไมถึงแก้ปัญหา “logic ซ้ำ 2 service”: การตัดสินว่า flow อยู่ขั้นไหนเกิดที่ saga ที่เดียว แล้ว push สถานะลง Lego — Lego ไม่คำนวณ in-flight status เอง
1.5 บทบาทแต่ละ Service (อ่านส่วนของตัวเองได้)
ทุกทีมอ่านแถวของตัวเองเพื่อรู้ว่า “ฉันต้อง produce/consume อะไร + รับผิดชอบอะไร”
| Service | Consume (รับเข้า) | Produce (ส่งออก) | เป็นเจ้าของ | ไฟล์ |
|---|---|---|---|---|
| Lego (UserService) | progress (flow-progress-update) + decision (flow-approval-result) จาก Orch | flow-submitted + flow-cancel-requested → Orch · notify requestor | status machine (mirror in-flight), automation chain, requestor notification | 01 |
| Orchestrator | flow-submitted/flow-cancel-requested (Lego) · Pickup/SentToReviewer/Decision/Cancel (Workflow) | progress+decision → Lego · create/rework/close → Task · create/resubmit/force-stop → Workflow | saga ProcessState (= source of truth in-flight), 2-actor tracking, 4-eye gate, idempotency/teardown | 02 |
| Workflow 🤝 | HTTP จาก Orch: create / resubmit / force-stop | ASB → Orch: Pickup, SentToReviewer, Decision (แปลงจาก native APPROVE/REJECT) · notify maker (pool + assignee) | phase/step model + assignee, การแปลง native→semantic event, maker notification | 03 |
| Task 🤝 | HTTP จาก Orch: create / mark-rework / complete / close | (return TaskID) | task lifecycle + idempotent close/complete | 04 |
ลำดับเหตุการณ์ย่อ (ทุก service เห็นจังหวะของตัวเอง):
Lego submit → Orch สร้าง Task+Workflow → Workflow: maker หยิบ→Pickup→ส่ง reviewer→SentToReviewer→ตัดสิน→Decision → Orch project/command กลับ Lego + ปิด Task → Lego หมุน status + notify requestor
2. Status Mapping — ตารางกลาง (contract หลัก)
| # | Lego FlowInstanceStatus | Saga ProcessState | Trigger (ทิศทาง) | Channel | Notify (ใคร→ใคร) |
|---|---|---|---|---|---|
| 1 | Draft | (ยังไม่มี saga) | Lego local | — | — |
| 2 | Submitted | AwaitingPickup | Lego→Orch flow-submitted(first) | — | Workflow→maker pool |
| 3 | InReview ⚠️ | UnderReview | Workflow→Orch Pickup → Orch→Lego | Progress | — |
| 4 | Reviewed | UnderConsideration | Workflow→Orch SentToReviewer → Orch→Lego | Progress | — |
| 5 | Resubmitted | UnderReview (re-enter) | Lego-local ตอนกด resubmit | (local) | Workflow→maker (assignee) |
| 6 | Approved | Approved (Completed) | Workflow→Orch Decision(Approve) → Orch→Lego | Decision | Lego→requestor |
| 7 | Rejected | Rejected (Completed) | Workflow→Orch Decision(Reject) → Orch→Lego | Decision | Lego→requestor |
| 8 | Rework | ReworkRequested | Workflow→Orch Decision(Rework) → Orch→Lego | Decision | Lego→requestor |
| 9 | Cancelled | Cancelled | 2 ขา (§6) → Orch fan-out | Decision | Lego→requestor |
| 10 | Finalized | (Completed แล้ว) | Lego local (automation จบ) | — | — |
| — | Abandoned | (ไม่แตะ) | Lego local housekeeping | — | — |
| — | Closed | — | เผื่อไว้ ยังไม่ wire | — | — |
⚠️
InReview= open item (grill scope): มีที่มาเฉพาะถ้า Workflow มี pickup/claim — ถ้า Workflow role-based อาจ map = “phase แรก IN_PROGRESS” หรือตัดทิ้ง (ดู CONTEXT.md Open item) Persistence:FlowInstance.Status=HasConversion<string>().HasMaxLength(30)→ เพิ่ม enum ใหม่ = code-only ไม่มี migration ✅ ⚠️ แก้จาก plan-phase finding:FlowSnapshotJsonเป็น immutable snapshot (D-V2-4, set ครั้งเดียวตอน Create) → เก็บ progress metadata ที่นั่นไม่ได้. metadata ที่ต้องการ:lastProgressAt(ordering guard) +assignedMaker/assignedReviewer(display) — 3 ทางเลือก: (a) jsonb column ใหม่ = migration 1 ตัว · (b) แนะนำ derivelastProgressAtจากFlowInstanceHistory.OccurredAtที่มีอยู่แล้ว (no migration) · (c) ไม่ persist (เสีย ordering guard). ตัดสินก่อน execute (ดู 01 §L2)
3. สอง Channel ของ Orch→Lego (แยกขาด)
| Channel | Topic | สำหรับ | Side effect | ผ่าน Validator |
|---|---|---|---|---|
| Progress projection | flow-progress-update (ใหม่) | InReview, Reviewed | status-only, idempotent | ❌ ไม่ผ่าน |
| Decision command | flow-approval-result (เดิม) | Approve/Reject/Rework/Cancel | automation/teardown | ✅ ผ่าน (ขยาย in-flight set) |
Resubmittedไม่อยู่ทั้ง 2 channel — Lego set เองตอนกดปุ่ม (symmetric กับSubmitted) Notification ไม่ผ่าน channel ไหนของ orchestrator — Lego/Workflow ยิงเองตาม audience (ดู §ownership ด้านล่าง)
กฎ ordering (Progress channel): ใช้ occurredAt — apply เฉพาะเมื่อ occurredAt > lastProgressAt และ status ยังไม่ terminal; reviewer→maker ถอยกลับได้ (Reviewed→InReview) จึงไม่ใช้ rank monotonic
3.5 Notification Ownership (data-ownership split) [grill 2026-06-15]
orchestrator ไม่ทำ notification เลย — แต่ละ service ยิงเฉพาะ audience ที่ตัวเองถือ data:
| Audience | ผู้ยิง | data ที่ถือ | ยิงเมื่อ |
|---|---|---|---|
| Requestor | Lego | requestorUserId + email + deep link resume | Approve/Reject/Rework/Cancel |
| Maker pool | Workflow | maker pool | งานเข้ากองกลาง (create) |
| Assignee (Maker) | Workflow | assignee identity | resubmit/resume |
| — | Orchestrator | — | ไม่ยิงเลย |
เหตุผล: “split by audience” ≠ “logic ซ้ำ” — แต่ละฝั่ง notify คนที่ตัวเองมี contact data; ผล: orchestrator ตัด
INotifyPort+IMakerDirectoryPort+ ตัด Phase-0 dep “maker role name”
4. Sequence Diagrams
4.1 Happy Path — Submit → Approve → Finalized
sequenceDiagram
autonumber
participant U as Requestor
participant L as Lego (UserService)
participant O as Orchestrator (Saga)
participant W as Workflow (+Task, +notify maker)
participant N as Notification
U->>L: Submit
L->>L: Status = Submitted
L-)O: ASB flow-submitted (first)
Note over O: Saga create → AwaitingPickup
O->>W: HTTP create task + workflow instance (unassigned)
W-)N: notify maker pool (Workflow เป็นคนยิง)
Note over W: Maker หยิบงาน (ถ้ามี pickup — open item)
W-)O: ASB Pickup (+maker identity)
Note over O: UnderReview / track MakerUsername
O-)L: ASB flow-progress-update (InReview)
L->>L: Status = InReview (mirror)
Note over W: Maker approve phase 1 = ส่งให้ reviewer
W-)O: ASB SentToReviewer (+reviewer identity)
Note over O: UnderConsideration / track ReviewerUsername
O-)L: ASB flow-progress-update (Reviewed)
L->>L: Status = Reviewed (mirror)
Note over W: Reviewer approve phase สุดท้าย
W-)O: ASB Decision(Approve, decisionId)
Note over O: Approved / Completed
O->>W: HTTP complete task
O-)L: ASB flow-approval-result (APPROVED)
L->>L: Status = Approved → fire automation chain
L-)N: notify requestor (Approved) — Lego เป็นคนยิง
L->>L: automation เสร็จ → Finalized
4.2 Rework Loop — Rework → Resubmit
sequenceDiagram
autonumber
participant U as Requestor
participant L as Lego
participant O as Orchestrator
participant W as Workflow
participant N as Notification
Note over W: Maker/Reviewer ตีกลับ (REJECT + OnRejectToPhaseOrder>0)
W-)O: ASB Decision(Rework, targets, note)
Note over O: ReworkRequested
O-)L: ASB flow-approval-result (REWORK, targets)
L->>L: Status = Rework (owner เปิดแก้)
L-)N: notify requestor (Rework) — Lego ยิง (มี deep link)
U->>L: แก้ step + กด Resubmit
L->>L: Status = Resubmitted (local, Rework→Resubmitted)
L-)O: ASB flow-submitted (IsResubmit=true)
Note over O: ExpectState ReworkRequested → re-enter UnderReview
O->>W: HTTP resubmit (resume) — ไม่สร้าง task/wf ใหม่
W-)N: notify maker (assignee คนเดิม) — Workflow ยิง
Note over O,W: วนกลับเข้า review cycle
4.3 Cancel — 2 ขา (Lego-initiated + Workflow-initiated)
sequenceDiagram
autonumber
participant A as Admin / Background
participant L as Lego
participant O as Orchestrator
participant W as Workflow
participant T as Task
participant N as Notification
rect rgb(235,245,255)
Note over A,N: ขา A — Lego-initiated (admin@Lego หรือ Lego sweep)
A->>L: Cancel (post-submit)
L->>L: Status = Cancelled (optimistic local) + teardown identity
L-)N: notify requestor (Cancelled) — Lego ยิง
L-)O: ASB flow-cancel-requested (initiatedBy=Lego, mode)
Note over O: OnCancel (idempotent) → Cancelled
O->>T: HTTP close task (idempotent)
O->>W: HTTP force-stop (idempotent)
O-)L: ASB flow-approval-result (CANCEL) → no-op (Cancelled แล้ว)
end
rect rgb(255,245,235)
Note over A,N: ขา B — Workflow-initiated (admin@Workflow หรือ Workflow sweep)
A->>W: Cancel
W->>W: cancel local
W-)O: ASB Decision(Cancel) / workflow-cancel
Note over O: OnCancel (idempotent) → Cancelled
O->>T: HTTP close task (idempotent)
O-)L: ASB flow-approval-result (CANCEL)
L->>L: Status = Cancelled + teardown identity
L-)N: notify requestor (Cancelled) — Lego ยิง
end
Double-sweep: ทั้ง 2 ขา sweep พร้อมกัน →
OnCancelตัวที่ 2 = no-op (idempotent) + teardown ports idempotent (รวมถึงทน “target ยังไม่ถูกสร้าง” กรณี submit-then-immediately-cancel)
4.4 Out-of-order Progress (occurredAt guard)
sequenceDiagram
autonumber
participant O as Orchestrator
participant ASB as ASB (no order guarantee)
participant L as Lego (progress consumer)
O-)ASB: progress InReview (occurredAt=T1)
O-)ASB: progress Reviewed (occurredAt=T2, T2>T1)
ASB-)L: Reviewed (T2) มาก่อน
L->>L: lastProgressAt = T2 → Status = Reviewed
ASB-)L: InReview (T1) มาทีหลัง
L->>L: T1 < lastProgressAt(T2) → IGNORE
Note over L: reviewer→maker ถอยจริง = occurredAt ใหม่กว่า → apply ได้
5. Saga State Machine (OnboardingApprovalSaga) — 2-actor + 4-eye
stateDiagram-v2
[*] --> AwaitingPickup: flow-submitted (first)
AwaitingPickup --> UnderReview: Pickup / track Maker + Progress(InReview)
UnderReview --> UnderConsideration: SentToReviewer / track Reviewer + Progress(Reviewed)
UnderConsideration --> UnderReview: (reviewer bounce — occurredAt)
UnderConsideration --> Approved: Decision(Approve) / +TaskComplete
UnderReview --> Rejected: Decision(Reject) / +TaskClose
UnderConsideration --> Rejected: Decision(Reject) / +TaskClose
UnderReview --> ReworkRequested: Decision(Rework)
UnderConsideration --> ReworkRequested: Decision(Rework)
ReworkRequested --> UnderReview: flow-submitted (resubmit) / WorkflowResubmit
AwaitingPickup --> Cancelled: Cancel
UnderReview --> Cancelled: Cancel
UnderConsideration --> Cancelled: Cancel
ReworkRequested --> Cancelled: Cancel
Approved --> [*]
Rejected --> [*]
Cancelled --> [*]
4-eye gate (enforce ที่ saga): Approve รับเฉพาะ UnderConsideration (Reviewer เท่านั้น — Maker approve เองไม่ได้); Reject/Rework รับ {UnderReview, UnderConsideration} (Maker kick-back ได้)
ของใหม่ใน saga: ProcessState UnderConsideration · inbound event SentToReviewerEvent · outbox event LegoProgressRequested · track MakerUsername + ReviewerUsername (2-actor) · OnPickup project InReview · OnDecision เพิ่ม TaskComplete(Approve)/TaskClose(Reject) · 4-eye ExpectState gate
6. Cancel Coordination — สรุปกฎ
| ผู้สั่ง | Lego status | saga? | พฤติกรรม |
|---|---|---|---|
| Owner | Draft | ❌ | Lego local ล้วน (ยังไม่มี Task/Workflow) |
| Admin@Lego / Lego sweep | post-submit | ✅ | optimistic local + publish flow-cancel-requested → Orch fan-out teardown |
| Admin@Workflow / Workflow sweep | — | ✅ | Workflow cancel + publish → Orch fan-out + project CANCEL → Lego |
Idempotency 3 ชั้น: (1) OnCancel ignore ถ้า Cancelled แล้ว · (2) teardown ports no-op ถ้าปิดแล้ว · (3) teardown ทน “ยังไม่ถูกสร้าง” · fan-out ทั้ง 3 เสมอ ไม่เช็คผู้เริ่ม
post-submit cancel set (Lego validator): {Submitted, InReview, Reviewed, Resubmitted, Rework}
7. Contract Matrix (Metrics — แต่ละ service คุยกันยังไง + ตรงกันไหม)
✅ = มี/ตรงแล้ว · 🔨 = ต้องสร้างใหม่ · ⚠️ = มีแต่ต้องแก้ · 🤝 = contract ต้องคุยกับทีมเจ้าของ (Workflow/Task)
7.1 Message / Endpoint inventory
| # | ชื่อ | Transport | Topic/Path | Publisher/Caller | Consumer/Callee | Payload (field หลัก) | สถานะ |
|---|---|---|---|---|---|---|---|
| M1 | flow-submitted | ASB | flow-submitted-for-approval | Lego ✅ | Orchestrator 🔨 | eventId, flowInstanceId, refNo, flowDefinitionCode, isResubmit | ⚠️ เพิ่ม consumer (Lego ไม่ต้องใส่ requestorUserId — Lego notify เอง) |
| M2 | flow-cancel-requested | ASB | flow-cancel-requested (ใหม่) | Lego 🔨 | Orchestrator 🔨 | eventId, flowInstanceId, refNo, initiatedBy, mode, reason | 🔨 |
| M3 | Pickup | ASB | (ทีม Workflow กำหนด) | Workflow 🤝 | Orchestrator 🔨 | flowInstanceId/refNo, makerIdentity, occurredAt | 🤝 + ขึ้นกับ Workflow มี pickup ไหม |
| M4 | SentToReviewer | ASB | (ทีม Workflow) | Workflow 🤝 | Orchestrator 🔨 | flowInstanceId/refNo, reviewerIdentity, occurredAt | 🤝 (= APPROVE non-final phase) |
| M5 | Decision | ASB | (ทีม Workflow) | Workflow 🤝 | Orchestrator 🔨 | flowInstanceId/refNo, action, decisionId, reworkTargets, reason, occurredAt | 🤝 (Approve/Reject/Rework/Cancel — map จาก APPROVE/REJECT+OnRejectToPhaseOrder) |
| M6 | flow-progress-update | ASB | flow-progress-update (ใหม่) | Orchestrator 🔨 | Lego 🔨 | eventId, flowInstanceId, refNo, progressStatus(InReview/Reviewed), occurredAt | 🔨 |
| M7 | flow-approval-result | ASB | flow-approval-result (เดิม) | Orchestrator 🔨 | Lego ✅⚠️ | decisionId, flowInstanceId, result(APPROVED/REJECTED/REWORK/CANCEL), reworkTargets, reason | ⚠️ consumer มี แต่ขาด CANCEL |
| M9 | Task create | HTTP | (ทีม Task) | Orchestrator 🔨 | Task 🤝 | refNo, … → TaskID | 🤝 (มี endpoint, ยืนยัน contract) |
| M10 | Task rework | HTTP | (ทีม Task) | Orchestrator 🔨 | Task 🤝 | taskId/refNo | 🤝 |
| M11 | Task close/complete | HTTP | (ทีม Task) | Orchestrator 🔨 | Task 🤝 | taskId/refNo, outcome | 🤝 |
| M12 | Workflow create | HTTP | (ทีม Workflow) | Orchestrator 🔨 | Workflow 🤝 | document/refNo → WfInstanceId | 🤝 (ต้องคืน WfInstanceId) |
| M13 | Workflow resubmit | HTTP | (ทีม Workflow) | Orchestrator 🔨 | Workflow 🤝 | wfInstanceId | 🤝 |
| M14 | Workflow force-stop | HTTP | (ทีม Workflow) | Orchestrator 🔨 | Workflow 🤝 | wfInstanceId | 🤝 |
ตัดออกจาก grill (notification ownership):
M8 notification-personal,M15 maker directory— orchestrator ไม่ทำ notification; maker notify = Workflow ภายใน
7.2 Wire vocab — ต้องตรง 2 ฝั่ง
| Concept | Lego enum | Wire value (UPPER) | Orchestrator | ตรงกัน? |
|---|---|---|---|---|
| Approve | TransitionAction.Approve | APPROVED | DecisionAction.Approve | ✅ |
| Reject | TransitionAction.Reject | REJECTED | DecisionAction.Reject | ✅ |
| Rework | TransitionAction.Rework | REWORK / CORRECTION_REQUESTED | DecisionAction.Rework | ✅ (alias 2 ค่า) |
| Cancel | TransitionAction.Cancel | CANCEL | DecisionAction.Cancel | ⚠️ Lego consumer ขาด CANCEL (M7) |
| Progress | InReview/Reviewed | InReview/Reviewed | (saga ProcessState map) | 🔨 channel ใหม่ |
Workflow native → Decision mapping (ต้องคุยกับทีม Workflow): code ปัจจุบัน Workflow มีแค่
APPROVE/REJECT+ phase progression +OnRejectToPhaseOrder. การแปลงเป็น Decision vocab (SentToReviewer/Approve/Reject/Rework) = หน้าที่ Workflow publisher แปลงก่อน publish — ดู 03
8. Operational Metrics (ที่ควร monitor หลัง deploy) — แยกตาม Service
แต่ละทีม monitor metric ของตัวเอง; ตัวที่ cross-service ระบุ owner ไว้
8.1 Lego (UserService)
| Metric | สัญญาณอันตราย |
|---|---|
flow-progress-update apply lag (publish→apply) | ASB backpressure / consumer ช้า |
| Progress message ignored (occurredAt เก่า) rate | สูงผิดปกติ = ordering พัง / clock skew |
flow-approval-result DLQ | wire vocab ไม่ตรง (เช่น CANCEL ไม่ map) |
| requestor notify failure | IFlowStatusNotificationPublisher ล่ม |
flow-cancel-requested publish failure | cancel ไม่ถึง orchestrator → Task/Workflow ค้าง |
8.2 Orchestrator
| Metric | สัญญาณอันตราย |
|---|---|
flow-submitted DLQ count | consumer ไม่ทำงาน / flowCode ไม่อยู่ใน OwnedFlowCodes |
Saga stuck ใน AwaitingPickup เกิน N นาที | Workflow ไม่ publish Pickup / ไม่มีคนหยิบ |
Saga stuck ใน UnderReview/UnderConsideration เกิน N | Workflow ไม่ publish Decision |
| Cancel double-fire count | ปกติ (idempotent) — แต่ track ไว้ดู race |
| Task/Workflow create retry (idempotency skip) | Correlations tracking ทำงานไหม |
| Outbox message age (pending) | OutboxProcessor ตาย |
8.3 Workflow 🤝
| Metric | สัญญาณอันตราย |
|---|---|
| ASB publish failure (Pickup/SentToReviewer/Decision) | publisher ล่ม → orchestrator ไม่รู้ว่า maker/reviewer ทำอะไร → saga ค้าง |
| event ที่ map native→semantic ไม่ออก (เช่น phase advance แต่ไม่ publish SentToReviewer) | mapping logic พลาด → Lego status ไม่ขยับ |
| create ไม่คืน WfInstanceId | orchestrator correlate ไม่ได้ → force-stop/resubmit ทำไม่ได้ |
| force-stop ที่ไม่ idempotent throw | cancel teardown ล้ม (กรณี stop ซ้ำ / ยังไม่ถูกสร้าง) |
| maker notify failure (pool/assignee) | maker ไม่รู้ว่ามีงาน → งานค้างกองกลาง |
8.4 Task 🤝
| Metric | สัญญาณอันตราย |
|---|---|
| create ไม่คืน TaskID | orchestrator correlate ไม่ได้ |
| close/complete ที่ไม่ idempotent | cancel double-sweep / cancel-vs-create race → error แทน no-op |
| mark-rework ล้ม | task ไม่สะท้อนสถานะ rework |
อ่าน cross-service: saga ค้าง (8.2) มัก root-cause อยู่ที่ Workflow ไม่ publish (8.3); Task/Workflow ค้าง (8.3/8.4) มัก root-cause ที่ Lego cancel publish ล้ม (8.1)
9. Phase-0 Contract Lock + External Dependencies
ต้อง ratify ก่อนเริ่ม code ขนาน:
- Lego↔Orch contract (M1, M2, M6, M7) — field-level (user เป็นเจ้าของ — lock ได้เลย)
- Workflow↔Orch contract (M3-M5, M12-M14) + Task↔Orch (M9-M11) 🤝 — คุยกับทีม Workflow/Task:
- Workflow มี pickup/claim ไหม → ตัดสิน
InReview(status list) - Workflow แปลง native (APPROVE/REJECT/OnRejectToPhaseOrder) → Decision vocab ที่ไหน
- Workflow create คืน WfInstanceId
- Task rework/close/complete endpoints + miss-behavior ของ GET /ref
- Workflow มี pickup/claim ไหม → ตัดสิน
- OwnedFlowCodes จริง — saga ปัจจุบัน =
{"FX_ONBOARD"}แต่ flow จริง =NEW_REQUESTOR→ verify + แก้ + disjoint จากAutoApproveWorker
ตัดออกแล้ว (grill):
Maker role name— ย้ายเป็นเรื่องภายใน Workflow (orchestrator ไม่ notify maker)
10. Decisions Log (forks ที่ปิดแล้ว)
| # | Decision | เลือก | เหตุผล |
|---|---|---|---|
| D1 | สถาปัตยกรรม | Approach A saga-projected single status | control plane, ไม่ซ้ำ logic |
| D2 | status model | field เดียว (FlowInstanceStatus +4) | ไม่มี migration (string column) |
| D3 | Progress vs Decision | 2 channel แยก topic | กัน decision side-effect ยิงตอน progress |
| D4 | Resubmitted | status แยกจริง, Lego-local | resubmit → Resubmitted (grill: ตรงคำ user) |
| D5 | Notification owner | data-ownership split — Orch ไม่ยิงเลย (grill) | Lego→requestor, Workflow→maker; ตัด INotifyPort/MakerDir |
| D6 | Progress ordering | occurredAt | reviewer bounce ถอยกลับได้ |
| D7 | Cancel | optimistic local + orchestrator fan-out idempotent | UX ตอบสนอง + converge |
| D8 | Task close บน decision | OnDecision +TaskComplete/TaskClose | กัน task ค้าง |
| D9 | FlowInstanceId | stable ข้าม resubmit | ResubmitAsync reuse instance เดิม |
| D10 | Review actors | 2-actor (Maker + Reviewer) (grill) | 4-eye audit; track 2 identity |
| D11 | Decision authority | Maker kick-back ได้ (Reject/Rework), approve ไม่ได้ (grill) | 4-eye — enforce ที่ saga |
| D12 | Workflow/Task internals | out of grill scope (grill) | contract = ข้อเสนอไปคุยทีม |
11. งานแยก 4 service (ดูรายละเอียดในไฟล์ย่อย)
| Service | ของใหญ่ | ไฟล์ |
|---|---|---|
| Lego (UserService) | +4 status, progress consumer, validator widen, CANCEL wire, cancel publisher, +PublishRejected/Cancelled (requestor notify) | 01 |
| Orchestrator | Lane 2 (consumers + Lego/Workflow/Task ports — ไม่มี notify port) + saga 2-actor/4-eye | 02 |
| Workflow 🤝 | ASB publisher (map native→Decision) + maker notify + HTTP endpoints | 03 |
| Task 🤝 | rework/close/complete endpoints | 04 |