บทความ

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 โดยเฉพาะ

  1. เริ่มจาก Modular Monolith เสมอ — Phoenix + contexts แยกชัดเจน, umbrella app ถ้าจำเป็น
  2. ใช้ Ash Framework ถ้าเหมาะ — declarative resources + policies ทำให้ boundary ชัด refactor เป็น service ทีหลังง่ายขึ้นเพราะ domain model ชัด
  3. แยกเฉพาะที่เจอปัญหาจริง — compute-heavy service (เช่น report generation, media processing) ที่ scale คนละ pattern กับ API หลัก
  4. อย่าแยกเพราะกระแส — Netflix ทำ microservices ได้เพราะมีทีม 100+ คนและ SRE ทีม 5 คนเลียนแบบจะได้แค่ความซับซ้อน
  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 ทีหลังเมื่อรู้ว่าต้องการอะไรจริงๆ