← Back to blog
Good Choices That Aged Badly - Elixir Migration Case Study
elixir migration

Good Choices That Aged Badly - Elixir Migration Case Study

Daniil Popov March 23, 2026

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.

Share