Articles

Elixir monorepo vs microservices — a senior dev's take

A question I get often as someone who's written Elixir for 5+ years: "Should we split into microservices?" or "Will a monolith kill us?" My short answer is most teams shouldn't rush to split — but before you take my word for it, let's look at the real reasons, including the costs people usually forget to count.

Definitions first, so we're talking about the same thing

Monorepo / Modular Monolith — all code in one repo, deployed as one release (or a few), but organized into clear contexts/modules inside a single BEAM VM.

Microservices — independent services deployed separately, communicating over the network (HTTP/gRPC/message queues), each with its own database.

In the Elixir world there's also Umbrella apps — multiple apps in one repo, compiled separately but running on the same BEAM instance. It's the middle ground people overlook.

Pros of Monorepo/Modular Monolith

  • Very low cost — one deploy, simple CI, easy debugging, no service discovery, API gateway, or separate distributed tracing
  • Free transactional consistency — multiple operations in one process, one Ecto.Multi call finishes it — no saga/orchestration needed
  • Refactor freely — change a function signature and the compiler warns every caller; take advantage of Elixir's static typing
  • Elixir is built for this — the BEAM VM handles tens of thousands of processes on one machine; you don't need separate services for concurrency
  • Small teams move faster — no cross-team deploy coordination, no API versioning between services

Cons of Monorepo/Modular Monolith

  • Can't scale services independently — a heavy bottleneck (e.g., image processing) can't be scaled separately; you scale the whole system
  • Large blast radius — a bug in one module can affect the whole release if the supervision tree isn't solid
  • Big teams collide easily — lots of shared files, frequent merge conflicts once the system and team grow past ~15–20 people
  • Language lock-in — everything must be Elixir; embedding Python/Go teams is hard

Pros of Microservices

  • Independent deploys — team A ships their service without waiting for team B; one service failing doesn't take down the system
  • Scale specific parts — scale heavy services separately, saving infra cost long-term if load isn't uniform
  • Choose your stack — performance-critical services in Rust, plain CRUD in Elixir/Phoenix
  • Clear bounded contexts — if domains are truly separate (e.g., payments vs. notifications), boundaries stay clean over time

Cons of Microservices — the costs people forget

  • Network latency + failure modes — every cross-service call has latency, timeouts, retries, circuit breakers to design yourself
  • Distributed transactions are complex — Ecto.Multi doesn't work across services; you need sagas, outbox patterns, eventual consistency — much harder to debug
  • Infra cost jumps — K8s, service mesh, observability stack (centralized tracing, metrics, logging) costs at least 2–3x a monolith
  • Worse developer experience — running locally means composing many services or mocking APIs, slower onboarding, more complex CI/CD pipelines
  • Needs SRE/DevOps staff — if the team is all developers with no one watching infra, microservices become a burden, not a speedup
  • API versioning forever — contract changes between services must stay backward compatible forever, or you coordinate deploys every time

Cost perspective: real numbers

Simple math — a 5-person team building a mid-size e-commerce system:

  • Monolith — 1 server/node + Postgres + Redis, infra ~3–5k THB/month, CI ~30 min, deploy in 1 click
  • Microservices (5 services) — at least 5 pods + separate DBs + API gateway + message queue + observability, infra ~15–30k THB/month plus someone to maintain it

And that doesn't count dev time lost to: designing API contracts, debugging distributed issues, version migrations — which often eats30–50% of team capacity in the early months.

When to choose what — a decision framework

Choose Monorepo/Modular Monolith when:

  • Team < 10–15 people
  • Product-market fit still unknown — the domain keeps changing
  • Limited budget — infra + DevOps cost matters
  • You need speed — MVP, fast iteration
  • Mostly CRUD + business logic, not compute-heavy work

Choose Microservices when:

  • Team > 15–20 people, genuinely splittable by domain
  • Very different scale requirements (e.g., video processing vs. API)
  • You need fault isolation — one service failing shouldn't drag everything down
  • You have budget and infra people — SRE/DevOps ready
  • Domain boundaries are clear and stable, not being refactored often

Traps to watch out for

  • Distributed monolith — separate services but still share a DB or call each other in chains → you get microservices' complexity without any of the benefits. The most dangerous outcome.
  • Premature microservices — splitting on day one because "we'll grow" → spend 6 months on infra instead of product
  • Shared database across services — boundaries aren't real; refactoring later becomes very hard
  • Elixir microservices you don't need — BEAM handles concurrency beautifully in one VM; splitting because you think "we need to scale" when one VM is enough

Moving forward or backward: when to change course

Monolith → microservices should happen when you hit real pain: one service can't scale, teams keep colliding, frequent deploys affect each other. The safe way is the strangler fig pattern — gradually extract the services that actually have problems first, not a full rewrite.

Microservices → monolith (merging back) is real and sometimes right: if overhead exceeds benefits, the team shrank, or debugging became unbearable. Uber did it, Shopify did it — it's not failure, it's a smart decision when costs outweigh benefits.

What I'd recommend specifically for Elixir

  1. Always start with a Modular Monolith — Phoenix + clearly separated contexts, umbrella apps if needed
  2. Use Ash Framework if it fits — declarative resources + policies keep boundaries clean, making later extraction into services easier
  3. Split only where pain is real — compute-heavy services (report generation, media processing) that scale differently from the main API
  4. Don't split because it's trendy — Netflix can do microservices with 100+ engineers and SREs; a 5-person team copying them gets only complexity
  5. Review every 6–12 months — when real pain shows up, extract piece by piece, don't rewrite

Summary

Monorepo/Modular Monolith fits small-to-mid teams, limited budgets, unstable domains — you get speed, simplicity, and low cost, and Elixir/BEAM makes a monolith strong enough to handle serious load.

Microservices fit large teams, clearly separated domains, genuinely different scale needs, and infra capacity — but expect costs 2–3x higher.

For Elixir, I recommend starting modular monolith and extracting when real pain appears — the choice most senior devs make, because it optimizes for "ship product fast" first, then optimize architecture later once you know what you actually need.