This article is based on a talk I gave at ElixirConf EU 2026 in Málaga.
I wrote it as a shorter, standalone version of the talk so the main ideas and trade-offs are easier to find, read, and share. The full recording is embedded just below, and the slides are available in slideshare.
At Cabify, a corporate journey is more than a ride.
When someone books a car to catch a flight or a train, they are also buying certainty. If the car is late, every minute increases the chance of missing the connection.
For our operations team, this meant that finding and fixing delays quickly could make a real difference for business travellers.
From a useful prototype to a real product
We started with a simple monitoring dashboard in Looker Studio. It gave operations agents a view of upcoming guaranteed journeys and let them step in when something looked wrong.
The prototype showed that proactive intervention worked.
But it also had clear limits. It relied on regular polling, so the information on screen was often out of date. Frequent BigQuery refreshes also increased the cost of running it.
The workflow was not ideal either. To fix a journey, agents had to leave the dashboard and open another internal tool called Remedios. There was no assignment feature, so two agents could end up working on the same journey.
The prototype answered one question:
Does proactive intervention help?
Yes, it does.
The next question was more difficult:
How do we turn this workaround into a reliable product?
This was not just a dashboard
We had about two weeks, one backend engineer, and a system that had to provide:
Shared visibility for operations agents.
Fast updates.
Assignment and coordination.
Integration with existing workflows.
Reliable behaviour across a four-node cluster.
The system served a user base of roughly 90 operations users and handled about 30,000 guaranteed journeys per week.
At first, this looked like a dashboard problem. It was not. It was a coordination problem.
The system had to receive events, keep a live view of the journeys that mattered, keep users in sync, support filters and actions, and work across several nodes.
That changed how we looked at the technology choice.
Why not a JavaScript SPA?
Our first goal was to keep the number of moving parts low.
We did not want to split the work between a backend and a separate frontend application. We wanted less coordination, less context switching, and one engineer who could own the full flow.
A JavaScript SPA would have been possible. But for this internal tool and this deadline, it would have added work without solving the main problem better.
Go + HTMX was a good option
Go was a serious option. The team already knew it, the ecosystem was familiar, and Go with HTMX could have served a server-rendered internal tool without a separate SPA.
The hard part, however, was not rendering HTML. It was keeping operational state in sync across users and nodes.
With Go and HTMX, we would probably have needed more infrastructure for cross-node updates and coordination. That could have meant Redis or another broker, more synchronisation code, and more components to operate.
This did not make Go a bad choice. It meant that more of the coordination work would be ours to build and maintain.
With two weeks and one backend engineer, that added risk mattered.
Why LiveView fit the problem
Phoenix LiveView gave us a server-side UI model that matched the product.
Each connected agent could have its own LiveView state. Phoenix.PubSub could propagate updates across the cluster using BEAM distribution.
We did not need a custom API layer or a separate frontend-backend boundary for this tool. DaisyUI helped us move quickly on the interface and keep our attention on the operational workflow.
The result was a real-time dashboard with live updates and per-user state, while keeping the architecture small.
We did not choose Elixir because LiveView was fashionable. We chose it because the product needed live coordination, and the platform gave us a simple way to build it.
Elixir was the result of the reasoning, not the starting point.
How the system worked
We kept a few boundaries clear.
The database was the source of truth for persistent business state.
PubSub was used to spread changes quickly. It helped operators see changes sooner, but it did not replace the database.
LiveView kept a bounded amount of state for each connected user. It represented the active operational window, handled filters, received updates, and supported user actions. It did not keep the full history of every reservation alive.
For ETA and distance calculations, we used a singleton manager and reused cached results across LiveViews. This avoided duplicate requests.
The main flow was simple:
A business event showed that a guaranteed journey needed attention.
The journey was imported and saved.
PubSub told connected dashboards that the state had changed.
LiveViews updated their local state.
Agents took action, which was saved and broadcast again.
ETA-based signals were recomputed on a fixed cadence.
Not everything needed to be event-driven. We pushed important journey state changes and used periodic recomputation for values such as urgency windows and ETAs.
That was easier to understand and good enough for the product.
Scope was our first scalability decision
The tool did not model the full life of every reservation.
It focused on the high-risk window: the time when delay, driver assignment, ETA changes, or intervention mattered most. Once the rider had been picked up, the journey no longer needed to stay in the dashboard.
This reduced the amount of state we had to keep, limited unnecessary UI updates, and made the workflow clearer for agents.
Before tuning infrastructure, it is worth asking whether the product is carrying state that nobody needs.
A smaller problem is often a faster system.
Some choices were deliberately simple
The ETA manager started as a cluster-aware singleton. From a theoretical distributed-systems perspective, that is easy to criticise. It could become a bottleneck, and it might not be the right design for a much larger workload.
But the domain was still taking shape. We needed something correct, easy to understand, and quick to deliver. The singleton gave us that. If the workload grows, it can later evolve into a more parallel design.
The same idea applied to failure handling. With a four-node cluster, we could use BEAM supervision and clustering for part of the recovery work.
Failure did not disappear. We simply had less recovery machinery to build ourselves.
What I learned from the decision
The lesson is not that Elixir is better than Go.
The lesson is that a technology choice should be judged by the problems it removes as well as the features it provides.
Elixir and Phoenix LiveView made sense for this system because:
The user base was small and connected.
Shared, fast-changing state was central to the product.
The operational window was limited.
The delivery deadline was severe.
We wanted to avoid adding coordination infrastructure too early.
The platform could handle part of the distribution and recovery work for us.
In a different product or team, Go could be the better choice.
The question is not whether Elixir is the best language. The question is whether it lets you build this particular product with less extra architecture.
For us, the answer was yes.
I would be interested to hear how others evaluate this trade-off in their own systems.

