Good Choices That Aged Badly - Elixir Migration Case Study
Your Architecture Has a Headcount Problem
We inherited a SaaS. Accounting software, B2B, real customers, real data. Three developers keeping it alive (two backend, one frontend). Six Docker containers in production. Four programming languages across three separate applications talking to each other over gRPC.
It worked. That was the thing. Nothing was on fire. But three engineers were spending most of their time just keeping the lights on, and every new feature required touching all three codebases.
The CTO had been sitting on this problem for a while. Not because he didn't see it (he saw it clearly), but because "rewrite the whole thing" is a hard conversation to have when the system is technically functional and customers are paying.
We made the case for three weeks.
What the system actually looked like
The backend was FastAPI (Python). The frontend was React with Redux. Business logic (the actual workflows for document ingestion, data extraction, multi-tenant reporting) lived in a Java-based BPMN workflow engine called Zeebe, which needed its own monitoring UI (Camunda Operate) and its own data exporter (Elasticsearch, used for nothing else). Redis handled caching. MySQL handled storage.
Six services. Five .env files scattered across three applications. Two test runners. Two package managers. When something broke in production, you were correlating three separate log streams to find out where.
The frontend alone had 398 component files, 54 npm packages, and a Vite build pipeline that had its own opinions about Node versions.
When a compliance audit came up, drawing the data flow diagram took longer than fixing the thing that triggered the audit.
Three weeks
We started with the MySQL schema, not the code. Schemas are honest. They show you what the system actually does, regardless of what the application code thinks it's doing. From there we mapped the domain model, scaffolded a Phoenix 1.8 app, and had file upload, document processing, and basic accounting views working inside a week.
The next two weeks were feature parity: porting the Zeebe workflows into Oban background jobs, rebuilding the React frontend as LiveView pages, rewiring the LLM and OCR integrations.
The key architectural decision was Phoenix LiveView, which removes the separation between backend and frontend entirely. There's no REST API layer because there's no separate frontend application making HTTP calls. Business logic talks directly to the UI. Features that previously meant coordinating changes across three codebases (backend route, React component, Zeebe worker) now live in a single Elixir module.
That's where most of the numbers come from.
The numbers
| Before | After | |
| Total lines of code | 68,850 | 25,185 |
| Source files | 588 | 144 |
| Frontend code | 18,797 lines | 1,330 lines |
| Infrastructure config | 5,448 lines | 439 lines |
| Dependencies | 218 packages, 3 ecosystems | 78 packages, 1 |
| Docker services | 6 | 1 |
| Programming languages | 4 | 1 |
| Engineers to maintain | 3 | 1 |
The frontend reduction (−93%) isn't a rounding error. LiveView renders UI server-side. There's no React application. No Redux. No npm. The 398 component files became 3 HEEx templates and 3 JS files.
Nine things that stopped existing
Not reduced. Removed.
Zeebe + Camunda Operate. The workflow engine and its monitoring UI. Replaced by Oban, a job queue that runs inside the same BEAM process with no separate service required.
Elasticsearch. It was only there because Zeebe needed a data exporter. Zeebe went away; Elasticsearch went with it.
Redis. The BEAM VM manages in-memory state natively. There was nothing left to cache.
The React application. All of it. 398 components, the Redux store, the Vite build chain, the Node version management.
The REST API. 53 route files. Request serialisation. Response schemas. When there's no separate frontend making HTTP calls, there's no API to maintain.
gRPC. Used only for Zeebe inter-service communication. Gone when Zeebe went.
MySQL. Migrated to PostgreSQL.
Five .env files. Replaced by a single runtime.exs configuration file.
npm. No frontend, no Node, no package-lock.json merge conflicts.
The compliance piece
This doesn't show up in lines-of-code comparisons but it matters: every service, dependency, and data flow in your architecture is something an auditor has to map.
Six Docker services means six attack surfaces, six sets of CVEs to track, six things to document in a data flow diagram. Cross-service communication over gRPC and REST means access control boundaries to justify and logging to reconcile.
After the migration: two services, one language, function calls instead of HTTP requests. The data flow diagram fits on one page. The audit scope roughly halved, not because we did anything clever, but because there was just less stuff.
ISO 27001, SOC 2, GDPR. Any of these is coming eventually for a SaaS handling financial data. Simpler architecture makes that work cheaper.
What it cost
Three weeks of migration work, preceded by a paid assessment to scope the effort and confirm the approach made sense.
What the client got back: one engineer maintaining a system that previously needed three, a development environment that starts with mix phx.server instead of six Docker containers, and a codebase small enough that a new hire can read the whole thing in a day.
The original team made reasonable choices under the constraints they had. Python, React, and Zeebe were sensible picks in 2021. But technology stacks accumulate weight. At some point the maintenance cost of the architecture outpaces the cost of replacing it.
That's what we do.
Is your stack here?
The pattern usually shows up as: new features require touching three or more repos, your frontend and backend engineers can't cover for each other, spinning up a local environment takes more than ten minutes, and someone leaving the team means losing expertise in a specific service nobody else fully understands.
We start every engagement with a paid technical assessment: a structured look at what you have, what a migration would actually involve, and an honest answer about whether the numbers make sense for your business.
Details anonymised per NDA. The technical numbers are from the actual migration.