Elixir Monorepo vs Microservices — มุมมอง senior dev
คำถามที่ผมเจอบ่อยในฐานะคนเขียน Elixir มา 5+ ปี: "ควรแยก microservices เลยไหม?" หรือ "monolith เดียวจะตายไหม?" คำตอบสั้นๆ ของผมคือ ส่วนใหญ่ไม่ควรรีบแยกแต่ก่อนจะเชื่อผม มาดูเหตุผลกันแบบละเอียด รวมถึงต้นทุนที่คนมักลืมนับ
นิยามก่อนคุยกันให้ชัด
Monorepo / Modular Monolith — โค้ดทั้งหมดอยู่ repo เดียว deploy เป็น release เดียว (หรือไม่กี่ตัว) แต่แบ่งเป็น contexts/modules ชัดเจน ภายใน BEAM VM เดียว
Microservices — แยกเป็น service อิสระ deploy แยกกัน คุยกันผ่าน network (HTTP/gRPC/message queue) แต่ละตัวมี database ของตัวเอง
ในโลก Elixir มีอีกชั้นคือ Umbrella apps — หลาย app ใน repo เดียว compile แยกกัน แต่รันบน BEAM เดียวกัน เป็นทางกลางที่คนมองข้ามบ่อย
ข้อดีของ Monorepo/Modular Monolith
- ต้นทุนต่ำมาก — deploy ทีเดียว, CI ง่าย, debug ง่าย ไม่ต้องมี service discovery, API gateway, distributed tracing แยก
- Transactional consistency ฟรี — หลาย operation อยู่ใน process เดียว ทำ Ecto.Multi ทีเดียวจบ ไม่ต้อง saga/orchestration
- Refactor ได้เต็มที่ — เปลี่ยน function signature แล้ว compiler เตือนทุกจุดที่เรียก ใช้ประโยชน์จาก static typing ของ Elixir
- Elixir เก่งเรื่องนี้อยู่แล้ว — BEAM VM handle concurrency ระดับหมื่น process ในเครื่องเดียว ไม่ต้องแยก service เพื่อ scale concurrency
- ทีมเล็กทำงานเร็วกว่า — ไม่ต้อง coordinate deploy ข้ามทีม ไม่ต้อง version API ระหว่าง service
ข้อเสียของ Monorepo/Modular Monolith
- Scale ตาม service ไม่ได้ — bottleneck ที่หนัก (เช่น image processing) แยก scale ไม่ได้ ต้อง scale ทั้งระบบ
- Blast radius ใหญ่ — bug ใน module หนึ่ง อาจกระทบทั้ง release ถ้า supervision tree ไม่ดี
- ทีมใหญ่ชนกันง่าย — ไฟล์ร่วมกันเยอะ merge conflict ถี่ถ้าระบบใหญ่และทีม >15–20 คน
- ข้อจำกัดภาษา — ทุกอย่างต้องเป็น Elixir ถ้ามีทีม Python/Go ที่ต้องร่วมด้วยจะ embed ยาก
ข้อดีของ Microservices
- Deploy อิสระ — ทีม A deploy service ของตัวเอง โดยไม่รอทีม B ทั้งระบบไม่ล่มถ้าตัวหนึ่งพัง
- Scale เฉพาะจุด — service ที่หนัก scale แยกได้ ประหยัด infra cost ในระยะยาวถ้า load ไม่เท่ากัน
- เลือก tech stack ได้ — service ที่ต้อง performance สูง เขียน Rust, ส่วน CRUD ธรรมดาใช้ Elixir/Phoenix
- Bounded context ชัด — ถ้า domain แยกกันจริงๆ (เช่น payment vs notification) boundary ชัดเจนดีต่อ maintainability
ข้อเสียของ Microservices — ต้นทุนที่มักลืมนับ
- Network latency + failure modes — ทุก call ข้าม service มี latency, timeout, retry, circuit breaker ที่ต้องออกแบบเอง
- Distributed transaction ซับซ้อน — Ecto.Multi ใช้ไม่ได้ข้าม service ต้อง saga, outbox pattern, eventual consistency — debug ยากขึ้นมาก
- Infra cost พุ่ง — K8s, service mesh, observability stack (tracing, metrics, logging กลาง) อย่างน้อย 2–3x ของ monolith
- DevEx แย่ลง — run local ต้อง compose หลาย service หรือ mock API, onboarding ช้าลง, CI/CD pipeline ซับซ้อนขึ้น
- ต้องการทีม SRE/DevOps — ถ้าทีม dev ล้วนไม่มีใครดู infra microservices จะกลายเป็นภาระ ไม่ใช่ความเร็ว
- API versioning ตลอดชีพ — เปลี่ยน contract ระหว่าง service ต้อง backward compatible ตลอดกาล หรือต้อง coordinate deploy
มุมมองต้นทุน: คิดเป็นเงินจริง
ลองคิดง่ายๆ — ทีม 5 คน, ระบบ e-commerce ขนาดกลาง:
- Monolith — 1 server/node + Postgres + Redis infra ~3–5k บาท/เดือน, CI ~30 นาที, deploy 1 click
- Microservices (5 services) — อย่างน้อย 5 pods + DBs แยก + API gateway + message queue + observability infra ~15–30k บาท/เดือน + ต้องมีคนดูแลเพิ่ม
ตัวเลขนี้ยังไม่รวมเวลา dev ที่เสียไปกับ: ออกแบบ API contract, debug distributed issue, version migration ซึ่งมักกินเวลา 30–50% ของ capacity ทีม ในช่วงแรก
เมื่อไหร่ควรเลือกอะไร — decision framework
เลือก Monorepo/Modular Monolith เมื่อ:
- ทีม < 10–15 คน
- ระบบยังไม่รู้ product-market fit — domain ยังเปลี่ยนบ่อย
- budget จำกัด — infra + DevOps cost สำคัญ
- ต้องการ speed — MVP, iterate เร็ว
- ส่วนใหญ่คือ CRUD + business logic ไม่ใช่ compute-heavy
เลือก Microservices เมื่อ:
- ทีม > 15–20 คน แยกทีมตาม domain ได้จริง
- มี requirement scale ต่างกันมาก (เช่น video processing vs API)
- ต้องการ fault isolation — service หนึ่งพังไม่ลากทั้งระบบ
- มีงบและคนดู infra — SRE/DevOps พร้อมแล้ว
- domain boundary ชัดและ stable แล้ว ไม่ต้อง refactor บ่อย
กับดักที่ต้องระวัง
- Distributed monolith — แยก service แต่ยัง share DB หรือ call กันเป็น chain → ได้ความยากของ microservices แต่ไม่ได้ประโยชน์อะไรเลย อันตรายที่สุด
- Premature microservices — แยกตั้งแต่วันแรกเพราะ "อนาคตจะโต" แล้วเสียเวลา 6 เดือนกับ infra แทนที่จะทำ product
- Shared database ข้าม service — ทำให้ boundary ไม่จริง refactor ทีหลังยากมาก
- Elixir microservices โดยไม่จำเป็น — BEAM ทำ concurrency ได้ดีมากใน VM เดียว แยกเพราะคิดว่า "ต้อง scale" ทั้งที่จริง scale VM เดียวก็พอ
เดินหน้าหรือถอยหลัง: เมื่อไหร่ควรเปลี่ยน
จาก monolith → microservices ควรทำเมื่อเจอ pain จริง: service เดียว scale ไม่ไหว, ทีมเริ่มชนกันบ่อย, deploy บ่อยแล้วกระทบกัน วิธีที่ปลอดภัยคือ strangler fig pattern — ค่อยๆ แยก service ที่เจอปัญหาจริงออกมาก่อน ไม่ใช่ rewrite ทั้งระบบ
จาก microservices → monolith (merge กลับ) มีจริงและควรทำถ้า: overhead > ประโยชน์, ทีมเล็กลง, debug ยากเกินรับไหว Uber เคยทำ, Shopify เคยทำ — ไม่ใช่ความล้มเหลว เป็นการตัดสินใจที่ฉลาดถ้าต้นทุนสูงเกินประโยชน์
แนวทางที่ผมแนะนำสำหรับ Elixir โดยเฉพาะ
- เริ่มจาก Modular Monolith เสมอ — Phoenix + contexts แยกชัดเจน, umbrella app ถ้าจำเป็น
- ใช้ Ash Framework ถ้าเหมาะ — declarative resources + policies ทำให้ boundary ชัด refactor เป็น service ทีหลังง่ายขึ้นเพราะ domain model ชัด
- แยกเฉพาะที่เจอปัญหาจริง — compute-heavy service (เช่น report generation, media processing) ที่ scale คนละ pattern กับ API หลัก
- อย่าแยกเพราะกระแส — Netflix ทำ microservices ได้เพราะมีทีม 100+ คนและ SRE ทีม 5 คนเลียนแบบจะได้แค่ความซับซ้อน
- ทบทวนทุก 6–12 เดือน — ถ้า pain จริงเริ่มมา ค่อย extract ทีละตัว ไม่ใช่ rewrite
สรุป
Monorepo/Modular Monolith เหมาะกับ ทีมเล็ก-กลาง, budget จำกัด, domain ยังไม่ stable — ได้ speed, simplicity, ต้นทุนต่ำ และ Elixir/BEAM ทำให้ monolith แข็งแรงพอรับ load สูงมาก
Microservices เหมาะกับ ทีมใหญ่, domain แยกชัด, scale requirement ต่างกันจริง, มี infra capacity — แต่ต้องยอมรับต้นทุนที่สูงกว่า 2–3 เท่า
สำหรับ Elixir ผมแนะนำ เริ่ม modular monolith แล้วค่อย extract เมื่อเจอ pain จริง — เป็นทางเลือกที่ senior dev ส่วนใหญ่เลือก เพราะมัน optimize สำหรับ "ทำ product ให้เร็ว" ก่อน แล้วค่อย optimize architecture ทีหลังเมื่อรู้ว่าต้องการอะไรจริงๆ