Domain-Driven Design · Go · PostgreSQL · Redis
Restaurant Queue Management System
ทำไมถึงต้องเป็นระบบนี้?
ปัจจุบันมีบริษัทที่ขาย POS และระบบคิวมากมาย แต่ยังไม่มีใครเลยที่จำลองทุกสถานการณ์จริงในหน้าจอเดียว ระบบนี้คือ ผู้บุกเบิก — ออกแบบเพื่อให้นักพัฒนาเรียนรู้ logic ของระบบคิว ร้านอาหาร และ POS ทั้งหมดผ่าน API จริง แบบ end-to-end
ปัจจุบันมีบริษัทที่ขาย POS และระบบคิวมากมาย แต่ยังไม่มีใครเลยที่จำลองทุกสถานการณ์จริงในหน้าจอเดียว ระบบนี้คือ ผู้บุกเบิก — ออกแบบเพื่อให้นักพัฒนาเรียนรู้ logic ของระบบคิว ร้านอาหาร และ POS ทั้งหมดผ่าน API จริง แบบ end-to-end
สารบัญ
- ทำไมต้อง DDD?
- Domain Map — ภาพรวม Subdomain
- Bounded Context Map
- Core: Queue Management
- Core: Table & Session
- Supporting: Order Management
- Supporting: Menu Management
- Generic: Identity & Auth
- Domain Events & SSE
- Background Workers
- Infrastructure Layer
- API Layer & Swagger
- Data Flow Diagrams
- Security Architecture
- Deployment Architecture
- Production Checklist ✅
1. ทำไมต้อง DDD?
ปัญหาของการออกแบบแบบเดิม (Anemic Domain Model)
❌ แบบเดิม (Transaction Script)
Controller → Service → Repository → Database
↓
สิ่งที่เกิดขึ้น:
- Logic ทุกอย่างกระจุกที่ Service layer
- Model เป็นแค่ data container (struct ว่างๆ)
- ไม่มีกฎธุรกิจป้องกันตัวเอง
- State ผิดพลาดได้ทุกเมื่อ (เช่น ticket "called" ซ้ำ)
- Test ยาก เพราะ Logic ผูกกับ Database DDD แก้ปัญหาอย่างไร?
✅ DDD (Rich Domain Model)
Controller → Application Service → Domain Entity (มี Logic ในตัว) → Repository
↓
ผลลัพธ์:
- Entity ป้องกันตัวเองด้วย State Machine
- Business Rule อยู่ใกล้ Data ที่สุด
- ทดสอบ Logic ได้โดยไม่ต้องเชื่อม Database
- Ubiquitous Language — โค้ดพูดภาษาเดียวกับ Domain Expert
- ไม่มีทางสร้าง State ผิดพลาดได้จาก outside
สามประเภทของ Domain
| ประเภท | Domain ในระบบนี้ | ความสำคัญ |
|---|---|---|
| Core Domain (ความได้เปรียบทางธุรกิจ) | Queue Management, Table & Session | สูงสุด — คือเหตุผลที่ระบบนี้มีอยู่ |
| Supporting Domain (สนับสนุน Core) | Order Management, Menu Management | สูง — ใช้งานได้แต่ซื้อหรือสร้างเองก็ได้ |
| Generic Domain (ทั่วไป) | Identity & Auth, Audit Log | ต่ำ — ใช้ Library หรือ Service สำเร็จรูปได้ |
2. Domain Map
แผนภาพนี้แสดง ทุก Subdomain ในระบบ และความสัมพันธ์ระหว่างกัน
┌─────────────────────────────────────────────────────────────────────────┐
│ RESTAURANT QUEUE SYSTEM — DOMAIN MAP │
│ │
│ ┌──────────────────── CORE DOMAIN ────────────────────────────────┐ │
│ │ │ │
│ │ ┌────────────────────┐ ┌────────────────────────────┐ │ │
│ │ │ Queue Management │◄────►│ Table & Session Mgmt │ │ │
│ │ │ │ │ │ │ │
│ │ │ • QueueTicket │ │ • Table │ │ │
│ │ │ • Queue Stats │ │ • TableSession │ │ │
│ │ │ • NoShow Logic │ │ • QRGuest │ │ │
│ │ │ • Position Mgmt │ │ • BuffetExpiry Worker │ │ │
│ │ │ • NoShow Worker │ │ │ │ │
│ │ └───────────┬────────┘ └──────────────┬─────────────┘ │ │
│ │ │ │ │ │
│ └───────────────┼───────────────────────────────┼───────────────────┘ │
│ │ │ │
│ ┌───────────────┼─── SUPPORTING DOMAIN ─────────┼───────────────────┐ │
│ │ ▼ ▼ │ │
│ │ ┌────────────────────┐ ┌────────────────────────────┐ │ │
│ │ │ Order Management │◄────►│ Menu Management │ │ │
│ │ │ │ │ │ │ │
│ │ │ • Order │ │ • MenuCategory │ │ │
│ │ │ • OrderItem │ │ • MenuItem │ │ │
│ │ │ • QR Ordering │ │ • Buffet vs AlaCarte │ │ │
│ │ └────────────────────┘ └────────────────────────────┘ │ │
│ │ │ │ │
│ │ ┌────────────────────┐ ┌────────────────────────────┐ │ │
│ │ │ Restaurant Mgmt │ │ Feature Toggle │ │ │
│ │ │ • Restaurant │ │ • Dynamic feature flags │ │ │
│ │ │ • Multi-tenant │ │ • No restart required │ │ │
│ │ └────────────────────┘ └────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────── GENERIC DOMAIN ────────────────────────────────┐ │
│ │ │ │ │
│ │ ┌────────────────────┐ ┌────────────────────────────┐ │ │
│ │ │ Identity & Auth │ │ Audit Log │ │ │
│ │ │ • User │ │ • AuditLog (Immutable) │ │ │
│ │ │ • JWT + 2FA │ │ • every critical action │ │ │
│ │ │ • Session (Redis) │ │ │ │ │
│ │ └────────────────────┘ └────────────────────────────┘ │ │
│ │ │ │ │
│ └──────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
3. Bounded Context Map
Bounded Context คือ "ขอบเขต" ที่ภาษาและ Model มีความหมายชัดเจน ภายในขอบเขตนั้น
┌──────────────────────────────────────────────────────────────────────┐
│ BOUNDED CONTEXT MAP │
│ │
│ ┌─────────────────┐ Conform ┌──────────────────────────────┐ │
│ │ Auth Context │──────────────►│ Restaurant Context │ │
│ │ │ │ (Multi-tenant owner) │ │
│ │ User │ │ Restaurant, Table │ │
│ │ JWT Claims │ │ FeatureToggle │ │
│ │ 2FA OTP │ └────────────┬─────────────────┘ │
│ └─────────────────┘ │ │
│ │ Customer/Staff │
│ ┌───────────────────┼────────────────┐ │
│ ▼ ▼ │ │
│ ┌──────────────────────────────────────────────────────────────┐ │ │
│ │ Queue Context (Core) │ │ │
│ │ │ │ │
│ │ QueueTicket (Waiting→Called→Seated→Completed/Cancelled/ │ │ │
│ │ Expired/NoShow) │ │ │
│ │ QueueStats, Position, NoShowWorker │ │ │
│ └───────────────────────────┬──────────────────────────────────┘ │ │
│ │ "Seat(tableID)" │ │
│ ▼ │ │
│ ┌──────────────────────────────────────────────────────────────┐ │ │
│ │ Table & Session Context (Core) │ │ │
│ │ │ │ │
│ │ Table (Vacant→Occupied→Clearing→Vacant/Disabled) │ │ │
│ │ TableSession (QR Token, Buffet timer, OrderCutoff) │ │ │
│ │ QRGuest (individual identity per table) │ │ │
│ │ BuffetExpiryWorker │ │ │
│ └───────────────────────────┬──────────────────────────────────┘ │ │
│ │ "canOrder(sessionToken)" │ │
│ ▼ │ │
│ ┌──────────────────────────────────────────────────────────────┐ │ │
│ │ Order Context (Supporting) │ │ │
│ │ │ │ │
│ │ Order (Pending→Confirmed→Preparing→Served/Cancelled) │ │ │
│ │ OrderItem (Pending→Preparing→Ready→Served/Cancelled) │ │ │
│ └───────────────────────────┬──────────────────────────────────┘ │ │
│ │ "getMenu(restaurantID)" │ │
│ ▼ │ │
│ ┌──────────────────────────────────────────────────────────────┐ │ │
│ │ Menu Context (Supporting) │ │ │
│ │ MenuCategory, MenuItem │ │ │
│ │ Buffet mode / AlaCarte mode toggle │ │ │
│ └──────────────────────────────────────────────────────────────┘ │ │
│ │ │
│ ┌─────────────Cross-Cutting Concerns (Generic)───────────────────┐ │
│ │ AuditLog (immutable, every context writes here) │ │
│ │ CacheFront (Redis read-through cache, all contexts read here) │ │
│ │ EventHub (SSE broadcast, all contexts publish here) │ │
│ └────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
Context Integration Patterns
| From Context | To Context | Pattern | รายละเอียด |
|---|---|---|---|
| Auth | Restaurant | Conformist | Restaurant ใช้ UserID จาก Auth โดยตรง |
| Queue | Table/Session | Customer/Supplier | Queue เรียก Seat() → สร้าง TableSession |
| Table/Session | Order | Published Language | QR Token คือ "ภาษากลาง" ระหว่าง Session กับ Order |
| All | AuditLog | Open Host Service |
ทุก Context เขียน AuditLog ผ่าน AuditLogRepository
|
| All | EventHub | Open Host Service |
ทุก Context publish event ผ่าน EventHub.Broadcast()
|
| Service | CacheFront | Shared Kernel | CacheFront ใช้ร่วมกันทุก Service |
4. Core Domain: Queue Management
Aggregate Root: QueueTicket
QueueTicket คือ Aggregate Root หลักของ Queue Context มีหน้าที่รักษา invariant ทั้งหมด
State Machine ของ QueueTicket
┌─────────────┐
┌───►│ CANCELLED │
│ └─────────────┘
│
┌──────────┐ Call() │ ┌──────────┐ Seat(tableID) ┌──────────┐ Complete() ┌───────────┐
│ WAITING │──────────┼─►│ CALLED │────────────────►│ SEATED │─────────────►│ COMPLETED │
└──────────┘ │ └──────────┘ └───────────┘
│ │ │
Cancel() │ Cancel()
Expire() │ Expire()
│ │ MarkNoShow() (auto by NoShowWorker)
▼ │ │
┌──────────┐ │ ▼
│ EXPIRED │ │ ┌──────────┐
└──────────┘ │ │ NO_SHOW │
└─►└──────────┘
(Seat จาก Waiting ได้ด้วย — walk-in direct)
Terminal States (สถานะสุดท้าย ไม่สามารถเปลี่ยนได้อีก)
| Status | ความหมาย | ถูกตั้งโดย |
|---|---|---|
completed |
ลูกค้าทานเสร็จ จ่ายเงินแล้ว | Staff กดปิด session |
cancelled |
ลูกค้าหรือ Staff ยกเลิก | User / Staff |
expired |
หมดเวลารอคิว (เช่นระบบ purge คิวเก่า) | System |
no_show |
ถูกเรียกแล้วไม่มา จนเกิน timeout | NoShowWorker (auto) |
Invariants ที่ Entity บังคับ (ไม่ผ่าน Database)
// ตัวอย่าง: ไม่มีทางเรียก Call() บน ticket ที่ไม่ใช่ "waiting"
func (t *QueueTicket) Call() error {
if t.Status != QueueTicketStatusWaiting {
return ErrInvalidStateTransition // ← ป้องกัน state ผิดพลาด 100%
}
// ...
}
Queue Domain Rules (Business Invariants)
-
Idempotency Key: ทุก ticket มี
idempotency_key— ป้องกัน double-submit -
Optimistic Locking:
versionfield ป้องกัน race condition เมื่อหลาย staff เรียกคิวพร้อมกัน - Party Size: 1–50 คน (เกินกว่านี้จะ reject ที่ domain level)
- Position Management: เมื่อ ticket เปลี่ยนสถานะ ตำแหน่งคิวจะถูก recalculate
-
No-Show Auto-Mark: ถ้า
calledเกินQUEUE_NOSHOW_TIMEOUT_MINนาที —NoShowWorkerจะเปลี่ยนเป็นno_showอัตโนมัติ
QueueTicket Validation Rules
CustomerName : required (ไม่ว่างเปล่า)
PartySize : 1–50 (บังคับที่ domain layer ก่อนถึง DB)
EntryMode : "walk_in" | "remote" (enum ที่ validate ด้วย IsValid())
DiningType : "buffet" | "alacarte" (enum ที่ validate ด้วย IsValid())
IdempotencyKey: unique index ที่ DB — ป้องกัน duplicate
5. Core Domain: Table & Session Management
Aggregate Roots: Table และ TableSession
Table และ TableSession เป็น Aggregate แยกกัน มีความสัมพันธ์ผ่าน
TableID
State Machine ของ Table
┌──────────────────────────────────────────┐
│ │
┌─────────▼──┐ Occupy() ┌──────────┐ StartClearing() ┌──────────┐
│ VACANT │───────────►│ OCCUPIED │─────────────────►│ CLEARING │
└─────────┬──┘ └──────────┘ └────┬─────┘
│ │ │
Disable() Disable() FinishClearing()
│ │ │
▼ │ ▼
┌──────────┐ ├──────────────────────► VACANT (loop)
│ DISABLED │ │
└────┬─────┘ (ห้าม Disable table ที่ OCCUPIED)
│
Enable()
│
▼
VACANT
State Machine ของ TableSession (Buffet vs AlaCarte)
┌────────────────────────────────────────────────┐
│ SESSION LIFECYCLE │
│ │
Seat(tableID) │ ACTIVE SESSION │
──────────────────►│ │
│ ┌────────────────────────────────────────────┐│
│ │ IsQRTokenValid() = true ││
│ │ CanOrder() = true / false (cutoff check) ││
│ │ ││
│ │ [BUFFET] [A LA CARTE] ││
│ │ ExpiresAt = set ExpiresAt = nil ││
│ │ OrderCutoffAt = set OrderCutoffAt = nil ││
│ │ WarningAt = set WarningAt = nil ││
│ └────────────────────────────────────────────┘│
│ │
│ ┌────────────┼────────────┐ │
│ ▼ ▼ ▼ │
│ RequestBill() Expire() Close() │
│ (BuffetWorker) (Staff) │
│ └────────────┼────────────┘ │
│ ▼ │
│ SESSION CLOSED │
│ IsActive = false │
└────────────────────────────────────────────────┘
QRGuest — Identity ภายใน Session
TableSession (1) ──── (many) QRGuest
│
└── Nickname (เช่น "พ่อ", "น้องแก้ว")
└── GuestToken (unique per guest scan)
└── Orders ของ guest คนนี้
ประโยชน์:
- ลูกค้าสแกน QR แล้วตั้งชื่อตัวเอง
- ครัวรู้ว่าออเดอร์นี้ของใคร
- คิดเงินแยกตามคนได้
- ป้องกัน race condition: guest_token ไม่ซ้ำ
6. Supporting Domain: Order Management
Aggregate Root: Order (contains OrderItem)
Order คือ Aggregate Root — OrderItem เป็น Value
Object ภายใน Order เดียวกัน ต้องเปลี่ยนผ่าน Order เท่านั้น
State Machine ของ Order
┌───────────┐
┌─────────┐ Confirm() ┌───────────┐ │ CANCELLED │
│ PENDING │────────────►│ CONFIRMED │──► └───────────┘
└─────────┘ └─────┬─────┘ CanCancel()=true
│
StartPreparing()
│
▼
┌────────────┐ CancelOrder() ┌───────────┐
│ PREPARING │──────────────────►│ CANCELLED │
└─────┬──────┘ └───────────┘
│
MarkServed()
│
▼
┌────────────┐
│ SERVED │ (Terminal — ห้าม Cancel)
└────────────┘
State Machine ของ OrderItem
┌─────────┐ StartPreparing() ┌────────────┐ MarkReady() ┌───────┐ MarkServed() ┌────────┐
│ PENDING │───────────────────►│ PREPARING │──────────────►│ READY │──────────────►│ SERVED │
└────┬────┘ └─────┬──────┘ └───┬───┘ └────────┘
│ │ │
CancelItem() CancelItem() CancelItem()
│ │ │
└───────────────────────────────┴──────────────────────────┘
│
▼
┌───────────┐
│ CANCELLED │ (Terminal)
└───────────┘
Business Rules สำหรับ Order
-
Session Gate: ก่อน place order ต้อง
session.CanOrder() == true(ป้องกัน order หลังหมดเวลาบุฟเฟ่ต์) -
Buffet Menu Only: รายการที่
is_buffet_only = trueจะสั่งได้เฉพาะเมื่อ DiningType = buffet - Price Snapshot: Price ของ OrderItem คือ snapshot ณ เวลาที่สั่ง ป้องกันการเปลี่ยนราคา menu หลังสั่งแล้ว
-
Order Number: Auto-generate เช่น
ORD-20250219-001(เรียง sequential per restaurant)
7. Supporting Domain: Menu Management
Entities: MenuCategory, MenuItem
Restaurant (1) ──── (many) MenuCategory
│
MenuCategory (1) ──── (many) MenuItem
│
┌─────┴──────────────────────────┐
│ name, price, description │
│ is_available (toggle by staff)│
│ is_buffet_only │
│ prep_time_minutes │
└────────────────────────────────┘
CacheFront Integration (Uber-inspired)
Menu data อ่านบ่อยมาก และเปลี่ยนน้อย — เป็น Read-Heavy workload ที่เหมาะกับ CacheFront pattern:
Customer Request: GET /menu/:restaurantID
│
▼
┌─────────────┐ Cache HIT ┌──────────────┐
│ CacheFront │◄───────────────►│ Redis │ (TTL: 5 นาที)
│(Read-Through)│ └──────────────┘
└──────┬──────┘
│ Cache MISS
▼
┌─────────────┐
│ PostgreSQL │
└─────────────┘
│
▼ populate cache
┌─────────────┐
│ Redis │
└─────────────┘
Circuit Breaker: ถ้า Redis down → ข้ามไปอ่าน DB โดยตรง (ไม่ crash)
8. Generic Domain: Identity & Auth
Entity: User, DeletedUser
User
├── ID, Email, Password (bcrypt cost=14)
├── TwoFactorEnabled
└── Deleted → DeletedUser (soft delete, 14-day backup)
Authentication Flow (JWT + 2FA Email OTP)
Register/Login Flow:
─────────────────────
Client ──POST /auth/register──► API
│
Hash bcrypt(cost=14)
Validate email format
Check duplicate email
│
Insert User to PostgreSQL
│
Client ◄──201 Created──────── API
Login Flow (with 2FA):
─────────────────────
Client ──POST /auth/login──► API
│
ValidateToken JWT (HMAC-only, no algorithm confusion)
bcrypt.CompareHash
│
┌──────────▼──────────┐
│ 2FA enabled? │
└──────────┬──────────┘
NO │ YES
│ │ │
│ │ Send OTP via Resend Email
│ │ Store OTP in Redis (TTL: 5 min)
│ │
│ │ Client ──POST /auth/verify-otp──►
│ │ │
│ └────────────────┘
▼
Generate TokenPair:
┌─────────────────────────────────────────┐
│ AccessToken = JWT (15 min, HS256) │
│ RefreshToken = random hex64 (in Redis) │
└─────────────────────────────────────────┘
│
Client ◄──200 OK + TokenPair──
Token Refresh:
─────────────
Client ──POST /auth/refresh──► API
│
Lookup RefreshToken in Redis
Verify not expired
Generate new AccessToken
Rotate RefreshToken (old invalidated)
│
Client ◄──200 OK + new TokenPair──
Security Layers
Rate Limiting : Redis-backed (100 req/min per IP, 1000 req/hour global)
CORS : Whitelist mode in production (CORS_ALLOWED_ORIGINS env)
JWT Algorithm : HMAC-only (prevents algorithm confusion attack)
Password bcrypt : cost=14 + max 72-byte enforcement (prevents bcrypt truncation bug)
Ownership Guard : middleware ตรวจ restaurantID == user.ownerID ทุก request
Panic Recovery : ทุก panic ถูก recover + log stack trace + return 500
Request ID : X-Request-ID ทุก request (tracing)
9. Domain Events & EventHub (SSE)
แผนภาพ Event Flow
┌────────────────────────────────────────────────────────────────────┐
│ DOMAIN EVENTS FLOW │
│ │
│ Domain Action Domain Event EventHub Broadcast │
│ ───────────── → ───────────── → ────────────────── │
│ │
│ Ticket Created → queue_updated → Staff Dashboard refresh │
│ Ticket Called → queue_updated → Customer display update │
│ Ticket Seated → queue_updated → Queue board update │
│ Ticket NoShow → queue_updated → Staff notification │
│ │
│ Order Placed → order_received → Kitchen display (new) │
│ Order Confirmed → order_status_changed → Customer QR view │
│ Order Preparing → order_status_changed → Customer QR view │
│ Order Served → order_status_changed → Customer QR view │
│ │
│ Session Warning → session_warning → Customer QR (time alert) │
│ Session Expired → session_warning → Staff + Customer │
│ │
│ Table Occupied → table_status_changed → Floor plan update │
│ Table Freed → table_status_changed → Floor plan update │
│ │
└────────────────────────────────────────────────────────────────────┘
SSE EventHub Architecture
┌───────────────────────────────┐
│ EventHub │
│ (in-process, goroutine-safe) │
│ │
│ clients: map[clientID]→Client│
│ byRestaurant: map[rID]→clients│
│ │
│ Broadcast(restaurantID, event)│
│ → serialise to JSON │
│ → send to all subscribers │
│ of that restaurant │
└─────────────┬─────────────────┘
│ SSE push (text/event-stream)
┌─────────────┼───────────────────────┐
▼ ▼ ▼
┌──────────────────┐ ┌──────────────┐ ┌────────────────────┐
│ Staff Dashboard │ │ QR Order │ │ Customer Queue │
│ (browser SSE) │ │ Page (SSE) │ │ Display (SSE) │
│ │ │ │ │ │
│ GET /events/:rid│ │ GET /events/ │ │ GET /queue/events │
└──────────────────┘ └──────────────┘ └────────────────────┘
Event Payload Structure
{
"type": "order_received",
"timestamp": "2025-02-20T10:30:00Z",
"data": {
"order_id": 42,
"table_number": "A3",
"guest_nickname": "น้องแก้ว",
"items": ["ข้าวต้ม x2", "ไก่ทอด x1"],
"total_amount": 280.0
}
}
10. Background Workers
Worker 1: BuffetExpiryWorker
ทุก BUFFET_CHECK_INTERVAL_SEC (default: 30 วินาที):
Step 1: FindSessionsNeedingWarning()
→ ดึง session ที่: IsActive=true AND WarningAt <= now AND WarningNotified=false
→ Broadcast EventSessionWarning ← แจ้งลูกค้าบน QR view
→ ตั้ง WarningNotified = true
→ บันทึก AuditLog "session.warning"
Step 2: FindExpiredSessions()
→ ดึง session ที่: IsActive=true AND ExpiresAt <= now AND DiningType=buffet
→ session.Expire() ← ปิด Session
→ table.StartClearing() ← โต๊ะเข้าสถานะ "กำลังทำความสะอาด"
→ CancelAllPendingOrders() ← ยกเลิก order ที่ค้างอยู่
→ Broadcast EventTableStatusChanged ← แจ้ง Floor plan
→ Broadcast EventSessionWarning ← แจ้งลูกค้า (session expired)
→ บันทึก AuditLog "session.expired"
Worker 2: NoShowWorker
ทุก BUFFET_CHECK_INTERVAL_SEC (default: 30 วินาที):
Step 1: FindTimedOutCalledTickets(QUEUE_NOSHOW_TIMEOUT_MIN)
→ ดึง ticket ที่: Status='called' AND CalledAt <= now - timeout
→ ticket.MarkNoShow() ← เปลี่ยน called → no_show
→ บันทึก AuditLog "queue.no_show"
→ Broadcast EventQueueUpdated ← แจ้ง Staff Dashboard
Worker Lifecycle & Graceful Shutdown
// ใน router.go
workerCtx, cancelWorkers := context.WithCancel(context.Background())
buffetWorker := service.NewBuffetExpiryWorker(...)
noShowWorker := service.NewNoShowWorker(...)
go buffetWorker.Start(workerCtx) // goroutine 1
go noShowWorker.Start(workerCtx) // goroutine 2
// เมื่อ SIGTERM/SIGINT:
cancelWorkers() // ทั้งสอง worker หยุดอย่าง graceful
11. Infrastructure Layer
Project Structure (DDD Layered)
my-api/
│
├── cmd/api/ ← Entry Point (Composition Root)
│ └── main.go → load config → connect DB/Redis → setup router → start server
│
├── internal/ ← ไม่ให้ package ภายนอก import ได้
│ ├── config/ ← Configuration (pure struct + env loader)
│ │ └── config.go
│ │
│ ├── model/ ← DOMAIN LAYER (Layer ที่สำคัญที่สุด)
│ │ ├── queue.go → Entities, State Machines, Domain Rules
│ │ ├── user.go → User Entity
│ │ └── errors.go → Domain Errors (ErrInvalidStateTransition, etc.)
│ │
│ ├── repository/ ← INFRASTRUCTURE LAYER (Database access)
│ │ ├── queue_repository.go
│ │ ├── order_repository.go
│ │ ├── session_repository.go
│ │ ├── table_repository.go
│ │ ├── menu_repository.go
│ │ ├── user_repository.go
│ │ ├── restaurant_repository.go
│ │ ├── audit_repository.go
│ │ ├── feature_toggle_repository.go
│ │ ├── qr_guest_repository.go
│ │ └── session_repository.go
│ │
│ ├── service/ ← APPLICATION LAYER (Use Cases / Orchestration)
│ │ ├── queue_service.go → CallNext, JoinQueue, SeatTicket
│ │ ├── order_service.go → PlaceOrder, UpdateStatus, CancelOrder
│ │ ├── qr_ordering_service.go → JoinSession, GetSessionDetail
│ │ ├── restaurant_service.go → CreateRestaurant, GetStats
│ │ ├── auth_service.go → Register, Login, Refresh, 2FA
│ │ ├── email_service.go → SendOTP via Resend API
│ │ ├── event_hub.go → SSE broadcast infrastructure
│ │ ├── buffet_worker.go → Background: buffet expiry
│ │ └── noshow_worker.go → Background: no-show detection
│ │
│ ├── handler/ ← INTERFACE LAYER (HTTP Handlers)
│ │ ├── auth.go
│ │ ├── health.go
│ │ ├── order.go
│ │ ├── queue.go
│ │ ├── qr_ordering.go
│ │ ├── restaurant.go
│ │ └── helpers.go
│ │
│ ├── middleware/ ← Cross-cutting HTTP concerns
│ │ ├── auth.go → JWT validation + claims injection
│ │ ├── cors.go → CORS (whitelist in prod)
│ │ ├── logger.go → Request logging
│ │ ├── ownership.go → Restaurant ownership guard
│ │ ├── ratelimit.go → Redis-backed rate limiting
│ │ ├── recovery.go → Panic recovery
│ │ ├── request_id.go → X-Request-ID header
│ │ └── swagger_auth.go
│ │
│ ├── router/ ← Route registration + DI wiring
│ │ └── router.go
│ │
│ ├── database/ ← DB connection + GORM auto-migrate
│ │ └── database.go
│ │
│ └── server/ ← HTTP server with graceful shutdown
│ └── server.go
│
├── pkg/ ← Reusable packages (ไม่ Business-specific)
│ ├── cache/
│ │ ├── cachefront.go → Read-through/Write-through cache
│ │ ├── circuit_breaker.go → Circuit Breaker (CLOSED/OPEN/HALF_OPEN)
│ │ ├── distributed_lock.go → Redis SETNX distributed lock
│ │ ├── redis.go → Redis client wrapper
│ │ └── metrics.go → Cache hit/miss/latency tracking
│ ├── logger/ → Structured logging
│ ├── response/ → Standardised JSON responses
│ └── utils/
│ ├── jwt.go → Token generation/validation
│ └── password.go → bcrypt hash/check + 72-byte guard
│
├── docs/ ← Swagger + DDD documentation
│ ├── swagger.json
│ ├── swagger.yaml
│ └── DDD.md ← (ไฟล์นี้)
│
├── public/ ← Static HTML simulation pages
│ ├── index.html → Portfolio landing page
│ ├── queue-simulation.html → จำลองลูกค้า join/call queue
│ ├── restaurant-game.html → จำลอง Staff dashboard
│ └── order.html → QR Ordering page
│
├── Dockerfile ← Multi-stage build (non-root user)
├── docker-compose.yml ← Postgres + Redis + API (healthchecks)
├── .env.example ← Template environment variables
├── .air.toml ← Hot-reload config
└── .golangci.yml ← Linter configuration
Database Schema — Entity Relationships
users
├── id, email, password, name, two_factor_enabled
└── deleted_users (soft-delete backup, 14 days)
restaurants
├── id, owner_id → users.id
├── name, slug (unique), dining_type
├── buffet_time_minutes, buffet_warning_minutes, buffet_order_cutoff_minutes
└── max_queue_size, is_active
tables
├── id, restaurant_id → restaurants.id
├── table_number, zone, capacity, min_guests
├── dining_type, status (vacant/occupied/clearing/disabled)
├── floor, is_smoking_area, sort_order, is_active
└── version (optimistic lock)
queue_tickets
├── id, restaurant_id → restaurants.id
├── ticket_number, customer_name, customer_phone
├── party_size, dining_type, entry_mode
├── status (waiting/called/seated/completed/cancelled/expired/no_show)
├── position, priority, estimated_wait
├── called_at, seated_at, completed_at, cancelled_at
├── table_id → tables.id (nullable, set after seating)
├── user_id → users.id (nullable, for remote booking)
├── idempotency_key (unique index — prevents duplicates)
└── version (optimistic lock)
table_sessions
├── id, restaurant_id, table_id → tables.id, ticket_id → queue_tickets.id
├── session_token (unique — QR Code payload)
├── dining_type, party_size
├── started_at, expires_at, order_cutoff_at, warning_at, ended_at
├── is_active, bill_requested, bill_requested_at
├── warning_notified, expire_notified
└── total_orders, total_amount, bill_total, total_guests, version
qr_guests
├── id, session_id → table_sessions.id
├── restaurant_id, nickname
├── guest_token (unique — per guest identity)
└── total_orders, total_amount
menu_categories
└── id, restaurant_id, name, sort_order, is_active
menu_items
├── id, restaurant_id, category_id → menu_categories.id
├── name, description, price, image_url
├── is_available, is_buffet_only, prep_time_minutes
└── sort_order
orders
├── id, restaurant_id, session_id, table_id, ticket_id
├── guest_id → qr_guests.id (nullable)
├── order_number, status, total_amount, item_count
├── notes, confirmed_at, preparing_at, served_at, cancelled_at
└── version
order_items
├── id, order_id → orders.id
├── menu_item_id → menu_items.id
├── name (snapshot), price (snapshot)
└── quantity, status, notes
feature_toggles
└── id, restaurant_id, feature_key, is_enabled, description, updated_by
audit_logs
└── id, restaurant_id, action, entity_type, entity_id
actor_id, actor_type, details (JSON), ip_address, created_at
Redis Key Patterns
Session (Auth):
session:{refreshToken} → userID (TTL: 7 วัน)
otp:{userID}:{type} → OTP code (TTL: 5 นาที)
Rate Limiting:
ratelimit:ip:{ip} → request count (TTL: 1 นาที)
ratelimit:global:{ip} → request count (TTL: 1 ชั่วโมง)
Cache (CacheFront):
menu:{restaurantID} → JSON menu (TTL: 5 นาที)
queue:stats:{restaurantID} → QueueStats (TTL: 10 วินาที)
restaurant:{id} → Restaurant (TTL: 10 นาที)
Distributed Lock:
lock:queue:{restaurantID} → locked/unlocked (TTL: 5 วินาที)
lock:table:{tableID} → locked/unlocked (TTL: 5 วินาที)
12. API Layer — Swagger & Route Map
Route Map ทั้งหมด
PUBLIC ROUTES (ไม่ต้อง Auth)
─────────────────────────────
POST /api/v1/auth/register → สมัครสมาชิก
POST /api/v1/auth/login → เข้าสู่ระบบ
POST /api/v1/auth/refresh → รีเฟรช access token
POST /api/v1/auth/verify-otp → ยืนยัน 2FA OTP
GET /health → Health check
GET /ready → Readiness probe (K8s/Docker)
CUSTOMER ROUTES (ไม่ต้อง Auth)
────────────────────────────────────
GET /api/v1/queue/:restaurantID/stats → ดูสถิติคิว (wait time, queue size)
GET /api/v1/queue/events/:restaurantID → SSE stream (real-time queue updates)
POST /api/v1/queue/:restaurantID/join → เข้าคิว (walk-in)
GET /api/v1/queue/ticket/:id/status → ดูสถานะตั๋วคิวของตัวเอง
QR ORDERING (token-based, no JWT)
───────────────────────────────────
POST /api/v1/qr/session/join → Join QR ordering session (scan QR)
GET /api/v1/qr/session/:token → ดู session detail (menu, time remaining)
POST /api/v1/qr/order → สั่งอาหาร (QR guest)
GET /api/v1/qr/orders/:guestToken → ดู orders ของตัวเอง
GET /api/v1/qr/events/:sessionToken → SSE stream (order status updates)
AUTHENTICATED ROUTES (ต้อง JWT Bearer Token)
──────────────────────────────────────────────
Restaurant Management:
POST /api/v1/restaurants → สร้างร้านอาหาร
GET /api/v1/restaurants → ดูร้านของตัวเอง
GET /api/v1/restaurants/:id → ดูข้อมูลร้าน
PUT /api/v1/restaurants/:id → แก้ไขข้อมูลร้าน
DELETE /api/v1/restaurants/:id → ลบร้าน
Table Management:
POST /api/v1/restaurants/:id/tables → สร้างโต๊ะ
GET /api/v1/restaurants/:id/tables → ดูโต๊ะทั้งหมด
PUT /api/v1/tables/:tableID → แก้ไขโต๊ะ
DELETE /api/v1/tables/:tableID → ลบโต๊ะ
PATCH /api/v1/tables/:tableID/status → เปลี่ยนสถานะโต๊ะ (clearing/vacant/disable)
Queue Management (Staff):
GET /api/v1/queue/:restaurantID → ดูคิวทั้งหมด (waiting, called, etc.)
POST /api/v1/queue/:restaurantID/call-next → เรียกคิวถัดไป
PATCH /api/v1/queue/ticket/:id/seat → นั่งโต๊ะ (เชื่อม ticket กับ table)
PATCH /api/v1/queue/ticket/:id/cancel → ยกเลิกตั๋ว
PATCH /api/v1/queue/ticket/:id/no-show → Mark no-show (manual)
Session Management (Staff):
GET /api/v1/sessions/:restaurantID → ดู active sessions
GET /api/v1/sessions/:id → ดู session detail
POST /api/v1/sessions/:id/request-bill → เรียกบิล
POST /api/v1/sessions/:id/close → ปิด session (จ่ายเงินแล้ว)
Order Management (Staff — Kitchen View):
GET /api/v1/orders/:restaurantID → ดู orders ทั้งหมด
GET /api/v1/orders/:id → ดู order detail
PATCH /api/v1/orders/:id/status → Update order status (confirm/prepare/serve)
DELETE /api/v1/orders/:id → Cancel order
Menu Management (Staff):
POST /api/v1/restaurants/:id/menu/categories → สร้าง category
GET /api/v1/restaurants/:id/menu/categories → ดู categories
POST /api/v1/menu/categories/:catID/items → สร้าง menu item
GET /api/v1/restaurants/:id/menu/items → ดู items ทั้งหมด
PUT /api/v1/menu/items/:itemID → แก้ไข item
PATCH /api/v1/menu/items/:itemID/availability → toggle available/unavailable
Feature Toggles:
GET /api/v1/restaurants/:id/features → ดู feature flags
PATCH /api/v1/restaurants/:id/features/:key → enable/disable feature
Audit Log:
GET /api/v1/restaurants/:id/audit → ดู audit log (owner only)
Cache Metrics (Admin):
GET /cache/metrics → CacheFront + Circuit Breaker stats
Swagger UI
เข้าถึงได้ที่: http://localhost:8080/swagger/index.html
Authentication ใน Swagger: คลิก Authorize → ใส่
Bearer <your_token>
13. Data Flow Diagrams
Flow 1: Customer เข้าคิว (Walk-in)
Customer API Service DB/Cache
│ │ │ │
│─POST /queue/join───►│ │ │
│ │─Validate Request────►│ │
│ │ │─Check Feature Toggle►│
│ │ │◄─── remote_booking ──│
│ │ │ │
│ │ │─Check Queue Stats ──►│ (Redis/DB)
│ │ │◄──── QueueStats ─────│
│ │ │ │
│ │ │─Acquire DistLock ───►│ Redis SETNX
│ │ │ │
│ │ │─Create QueueTicket ─►│ PostgreSQL
│ │ │◄─── Ticket(id=42) ───│
│ │ │ │
│ │ │─Release DistLock ───►│
│ │ │ │
│ │ │─Write AuditLog ─────►│
│ │ │ │
│ │ │─Broadcast SSE ──────►│ EventHub
│ │ │ (Staff dashboard updates)
│ │ │ │
│◄─201 Ticket Q042────│◄────────────────────│ │
Flow 2: Staff เรียกคิว (CallNext)
Staff App API QueueService DB + SSE
│ │ │ │
│─POST /call-next───►│ │ │
│ (restaurantID=5) │─Auth Middleware ──────►│ │
│ │ JWT + Ownership │ │
│ │───────────────────────►│ │
│ │ │─FindNextWaiting ───►│ Postgres
│ │ │◄─── ticket ─────────│
│ │ │ │
│ │ │─ticket.Call() ─────►│ (in-memory)
│ │ │ version++ │
│ │ │ status=called │
│ │ │ │
│ │ │─SaveTicket ────────►│ Postgres
│ │ │ │
│ │ │─UpdatePosition ────►│ (recalculate)
│ │ │ │
│ │ │─AuditLog ──────────►│
│ │ │ │
│ │ │─EventHub.Broadcast─►│ SSE → all clients
│ │ │ EventQueueUpdated │
│ │ │ │
│◄─200 CalledTicket──│◄──────────────────────│ │
│ │ │ │
│ [Customer's phone buzzes / display updates in real-time via SSE] │
Flow 3: QR Ordering (Customer)
Customer Phone API QROrderingService DB
│ │ │ │
│─Scan QR Code ─────────►│─POST /qr/session/join ►│ │
│ (session_token=abc) │ + nickname="น้องแก้ว" │ │
│ │ │─ValidateSession ──►│
│ │ │◄─ session(active) ─│
│ │ │ │
│ │ │─CreateQRGuest ────►│
│ │ │◄─ guest_token ─────│
│ │ │ │
│◄─ guest_token ─────────│◄───────────────────────│ │
│ │ │ │
│─GET /qr/session/abc ──►│ │ │
│ │─ValidateQRToken ───────►│ │
│ │ │─GetMenuWithCache ─►│ Redis/DB
│◄─ SessionDetail + Menu ─│ │ │
│ │ │ │
│─POST /qr/order ────────►│ │ │
│ guest_token + items │─CanOrder() check ──────►│ │
│ │ (is session active? │ │
│ │ is before cutoff?) │ │
│ │ │─CreateOrder ──────►│
│ │ │─EventHub.Broadcast►│ → Kitchen SSE
│◄─ 201 Order Created ───│ │ EventOrderReceived│
14. Security Architecture
Defence in Depth — ป้องกันหลายชั้น
Request
│
▼
┌──────────────────────────────────────────────────────────┐
│ Layer 1: Rate Limiter (Redis) │
│ 100 req/min per IP, burst protection │
└─────────────────────────────┬────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ Layer 2: CORS Guard │
│ Dev: allow all | Prod: whitelist only (env var) │
└─────────────────────────────┬────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ Layer 3: JWT Auth Middleware │
│ HMAC-only signing (ป้องกัน alg:none attack) │
│ Claims: userID (ตรวจสิทธิ์ทุก endpoint) │
└─────────────────────────────┬────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ Layer 4: Ownership Guard │
│ restaurantID param == auth.userID │
│ ป้องกัน IDOR (Insecure Direct Object Reference) │
└─────────────────────────────┬────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ Layer 5: Domain Validation │
│ Validate() ก่อน persist ทุกครั้ง │
│ State Machine บังคับ transition rules │
└─────────────────────────────┬────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ Layer 6: Panic Recovery │
│ ทุก panic ถูก recover → log stack trace → 500 │
└──────────────────────────────────────────────────────────┘
15. Deployment Architecture
Docker Compose (Local / Staging)
┌──────────────────────────────────────────────────────────────┐
│ docker-compose.yml │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ api (Go binary) │ │
│ │ - Multi-stage build (builder → runtime) │ │
│ │ - Non-root user (security best practice) │ │
│ │ - Port 8080 │ │
│ │ - healthcheck: GET /health (5s interval) │ │
│ │ - depends_on: postgres, redis (healthy) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────────┐ │
│ │ PostgreSQL 16 │ │ Redis 7 │ │
│ │ Port: 5432 │ │ Port: 6379 │ │
│ │ Volume: pg_data│ │ Volume: redis_data│ │
│ │ healthcheck: │ │ healthcheck: │ │
│ │ pg_isready │ │ redis-cli PING │ │
│ └─────────────────┘ └─────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
Environment Variables
# App
APP_ENV=production
PORT=8080
APP_BASE_URL=https://api.yourdomain.com # ← สำคัญ! ใช้ generate QR URL
# Database
DB_HOST=postgres
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=your_secure_password
DB_NAME=myapi
# Redis
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=your_redis_password
# JWT
JWT_SECRET=your_256bit_secret_key_minimum_32_chars
# Resend (Email 2FA)
RESEND_API_KEY=re_xxxxx
[email protected]
RESEND_FROM_NAME=My Restaurant
# CORS (production whitelist)
CORS_ALLOWED_ORIGINS=https://yourdomain.com,https://app.yourdomain.com
# Queue
QUEUE_MAX_SIZE=100
QUEUE_MAX_PARTY_SIZE=20
QUEUE_NOSHOW_TIMEOUT_MIN=10 # ← NoShowWorker จะ mark no-show หลังจาก 10 นาที
# Buffet
BUFFET_DURATION_MIN=90
BUFFET_WARNING_MIN=15
BUFFET_ORDER_CUTOFF_MIN=30
BUFFET_CHECK_INTERVAL_SEC=30 # ← Worker รัน check ทุก 30 วินาที
Production Checklist ก่อน Deploy
- ✅ ตั้ง APP_BASE_URL ให้ถูกต้อง (ใช้ generate QR Code URL)
- ✅ เปลี่ยน JWT_SECRET เป็น random string ยาวๆ (ไม่ใช่ default)
- ✅ ตั้ง CORS_ALLOWED_ORIGINS เป็น domain จริง
- ✅ ใช้ SSL/TLS (Nginx reverse proxy หรือ Cloudflare)
- ✅ ตั้ง DB password ที่แข็งแรง
- ✅ ตั้ง Redis password
- ✅ เปลี่ยน APP_ENV=production
- ✅ run
docker-compose up -d -
✅ ตรวจสอบ health:
curl https://api.yourdomain.com/health -
✅ ดู Swagger:
https://api.yourdomain.com/swagger/index.html
16. Checklist ความสมบูรณ์ระบบ
Domain Layer
| รายการ | สถานะ | รายละเอียด |
|---|---|---|
| QueueTicket State Machine | ✅ | 7 states, transitions ครบ, test ครอบคลุม |
| Table State Machine | ✅ | 5 states, optimistic lock |
| Order State Machine | ✅ | 6 states, item-level tracking |
| OrderItem State Machine | ✅ | 5 states |
| TableSession lifecycle | ✅ | Buffet timer, order cutoff, QR token validation |
| QRGuest identity | ✅ | Per-guest token, order attribution |
| Domain Validation (Validate()) | ✅ | ทุก entity ก่อน persist |
| Domain Errors (typed) | ✅ | ErrInvalidStateTransition, ErrInvalidPartySize, etc. |
| Idempotency (QueueTicket) | ✅ | unique_index idempotency_key |
| Optimistic Concurrency (Version) | ✅ | QueueTicket, Table, TableSession, Order |
Application Layer
| รายการ | สถานะ | รายละเอียด |
|---|---|---|
| QueueService (CallNext, Join, Seat) | ✅ | Distributed lock, Position recalc |
| OrderService (Place, Update, Cancel) | ✅ | Session gate check |
| QROrderingService | ✅ | Token validation, Buffet/Alacarte flow |
| RestaurantService | ✅ | Multi-tenant ownership |
| AuthService (JWT + 2FA) | ✅ | Refresh token rotation |
| EmailService (Resend) | ✅ | OTP delivery |
| BuffetExpiryWorker | ✅ | Warning + Expire auto loop |
| NoShowWorker | ✅ | Auto mark called→no_show |
| EventHub (SSE) | ✅ | All events broadcast |
Infrastructure Layer
| รายการ | สถานะ | รายละเอียด |
|---|---|---|
| PostgreSQL (GORM) | ✅ | Auto-migrate, all entities |
| Redis (go-redis/v9) | ✅ | Sessions, OTP, rate limit, cache |
| CacheFront (Read-through) | ✅ | Uber-inspired, menu/stats caching |
| Circuit Breaker | ✅ | CLOSED/OPEN/HALF_OPEN, FailureThreshold=5 |
| Distributed Lock (SETNX) | ✅ | Prevent race on CallNext |
| Rate Limiting (Redis) | ✅ | Per-IP + global |
| Audit Log (immutable) | ✅ | Every critical action |
| Feature Toggles | ✅ | Dynamic, without restart |
API & Docs Layer
| รายการ | สถานะ | รายละเอียด |
|---|---|---|
| Swagger UI | ✅ | /swagger/index.html |
| Swagger JSON/YAML | ✅ | /docs/swagger.json, swagger.yaml |
| All routes documented | ✅ | @Summary, @Description, @Tags ทุก handler |
| Health Check (/health) | ✅ | Status, uptime, version |
| Readiness Probe (/ready) | ✅ | DB + Redis ping (K8s ready) |
| Cache Metrics (/cache/metrics) | ✅ | Hit rate, circuit breaker stats |
| Standard Response Format | ✅ | {success, message, data, error, pagination} |
Security
| รายการ | สถานะ | รายละเอียด |
|---|---|---|
| JWT (HMAC-only) | ✅ | ป้องกัน alg:none attack |
| bcrypt (cost=14) | ✅ | ป้องกัน brute force |
| 72-byte password guard | ✅ | ป้องกัน bcrypt silent truncation |
| 2FA Email OTP | ✅ | Resend API, TTL 5 นาที |
| Ownership Guard | ✅ | IDOR protection |
| CORS Whitelist | ✅ | Production env |
| Rate Limiting | ✅ | Redis-backed |
| Panic Recovery | ✅ | ไม่ crash server |
| Request ID tracing | ✅ | X-Request-ID header |
DevOps
| รายการ | สถานะ | รายละเอียด |
|---|---|---|
| Dockerfile (multi-stage) | ✅ | แยก builder/runtime, non-root user |
| docker-compose.yml | ✅ | healthcheck, volume, depends_on |
| .env.example | ✅ | ครบทุก variable รวม APP_BASE_URL |
| .air.toml | ✅ | Hot-reload สำหรับ dev |
| .golangci.yml | ✅ | Linter config (errcheck, gosec, staticcheck, etc.) |
| Graceful Shutdown | ✅ | SIGTERM → workers stop → server drain |
| Structured Logging | ✅ | Level-based, request context |
Tests
| รายการ | สถานะ | Package |
|---|---|---|
| Domain State Machine | ✅ passed | internal/model |
| Middleware (CORS, Recovery, Logger) | ✅ passed | internal/middleware |
| Health Handler | ✅ passed | internal/handler |
| Circuit Breaker | ✅ passed | pkg/cache |
| Response Helpers | ✅ passed | pkg/response |
| JWT + Password utils | ✅ passed | pkg/utils |
ภาพรวมสุดท้าย — The Pioneer System
┌────────────────────────────────────────────────────────┐
│ │
│ Restaurant Queue + POS Simulation System │
│ "ระบบจำลองทุกสถานการณ์จริง ในหน้าจอเดียว" │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Customer │ │ Staff │ │ Kitchen │ │
│ │ Queue │ │Dashboard │ │ Display │ │
│ │ Display │ │(Restaurant│ │ (Orders)│ │
│ │ (SSE) │ │ Game) │ │ (SSE) │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ └─────────────┴─────────────┘ │
│ │ HTTP + SSE │
│ ▼ │
│ ┌──────────────────────────┐ │
│ │ Go API (this server) │ │
│ │ DDD + Clean Architecture│ │
│ └──────────┬───────────────┘ │
│ │ │
│ ┌─────┴─────┐ │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │PostgreSQL│ │ Redis │ │
│ │ (domain │ │ (cache + │ │
│ │ store) │ │ sessions│ │
│ └──────────┘ │ + locks)│ │
│ └──────────┘ │
│ │
└────────────────────────────────────────────────────────┘
สรุป: ระบบนี้ไม่ใช่แค่ CRUD API ธรรมดา — มัน encode ความรู้ของระบบร้านอาหารจริงๆ ลงไปใน domain model เองเลย ทุก business rule มีที่อยู่ที่ถูกต้อง ทุก state transition ถูกบังคับ ทุก race condition ถูกป้องกัน และทุกเหตุการณ์สำคัญถูกบันทึกไว้ในลักษณะ immutable นี่คือสิ่งที่ DDD ให้กับเรา