QIAOPANG.COM · SELECTED ESSAYS (2013–2026) · 260 ESSAYSBrowse Chronologically (2013 → 2026) ↗
QiaoPang · Programmers LifeSelected Essays · Systems, Craft & Agentic Era
Timeline ↗

Phoenix 1.3: Toward the Right Path

Phoenix 1.3: Toward the Right Path

It has been more than a year since 1.2 was released, and half a year since McCord announced 1.3’s features at ElixirConf 2016, yet Phoenix 1.3 still kept her face half-hidden behind a lute, reluctant to appear. A few days ago, 1.3 RC.0 was quietly released, and at last we can behold her beauty.

Introduction

Since most readers of Programmer’s Life are not Elixir/Phoenix users, let me offer a brief primer here. Elixir is a functional programming language that runs on the Erlang VM, has Ruby-like syntax, inherits all of Erlang’s capabilities, and supports metaprogramming. The Actor Model–based concurrency model of the Erlang VM (look it up on the wiki yourself), together with the power of pattern matching and the god’s-eye view afforded by metaprogramming, gives Elixir its unique charm. Phoenix, meanwhile, is a Rails-descended framework for Elixir that helps us build web apps quickly.

Compared with Rails, Phoenix offers two fundamental disruptions:

  1. Concurrency built on the Erlang VM. In many places you cannot see, it uses large numbers of processes, or process pools, to boost concurrency. In many cases, Phoenix’s performance is 5–10 times that of Rails. Phoenix’s author, McCord, ran an experiment and successfully achieved 2M WebSocket connections on a single machine.

  2. The realtime web. Phoenix greatly lowers the difficulty of building high-performance realtime apps. By abstracting out channels, it makes join / leave / broadcast / presence extraordinarily simple — so an ordinary engineer can write a highly scalable realtime app, such as a chat application supporting millions of users.

The following is a benchmark from an article (https://hashrocket.com/blog/posts/websocket-shootout). The test method is: each WebSocket, upon receiving a message, broadcasts it to all other WebSockets, then sends a status back to the sender. With 95th percentile response time < 500ms, it measures how many WebSockets can be supported:

Phoenix 1.3: Toward the Right Path

As we can see, Phoenix’s capacity—around 24,000 active WebSockets—trails C++ and Clojure, and runs neck and neck with Go.

But this benchmark is deeply unfair. Here, Phoenix and Rails are both full-fledged frameworks, while the others directly use language-level WebSocket libraries. The comparison is as absurd as running a network performance test, pitting UDP against TCP, and concluding that UDP performs far better than TCP. Considering that Phoenix abstracts channels on top of WebSockets, that each WebSocket connection is a pair of processes (one handling the network layer, one handling the channel layer), and that everything from connection to dispatch goes through the framework’s entire pipeline, achieving such efficiency is still quite astonishing.

The Evolution of the Directory Structure

Back to the main topic. Since Phoenix was born from Rails, every gesture and expression imitates its ancestor. Model, controller, view, template—nothing is missing, and the directory structure produced by scaffolding is strikingly similar. This brings many problems. The most important and fundamental one is: Are we building a system that includes a web interface, or an app centered on the web?

This is a question every web app must face as it grows. I mentioned in the article xxxx:

To put it bluntly, frameworks like Rails easily lead people down the wrong path of making the web frontend the center. By ‘frontend’ here, I mean the frontend of the backend. We should first build the business model according to requirements, and only after the services are properly constructed should we use Rails and the like to build the frontend. We may need a user-facing frontend, perhaps an admin-facing frontend, and each independent service may also need its own management frontend. We also need frontends for statistical analysis, user behavior analysis, and so on. None of these frontends really has so-called models, because data storage is handled within each service.

Let’s look at Phoenix 1.2’s directory structure:

Phoenix 1.3: Toward the Right Path

This is a typical web-centric approach. Your data models and various business logic seem to be aimed at a web interface. Although you can build an app quickly, over the long run there are many problems. Of course, as the system evolves, we extract business logic and data models, put them under lib, or even package them into independent apps in the idiomatic Elixir/Erlang way. Yet the scaffolded directory structure still deeply influences and constrains your code structure. At first, you stuff data models into web/models and various logic into web/controllers. Gradually, your code turns into this: business-handling logic and web-handling logic are kneaded together; logic between different models is kneaded together; and the controller must understand many model details to handle things properly:

Phoenix 1.3: Toward the Right Path

In code like this, we cannot see where the boundaries of the system’s parts lie. New code slips in so naturally that all good design becomes chaotic as boundaries blur. The ideal state looks like this:

Phoenix 1.3: Toward the Right Path

Business and web are separated; Blog looks more like a service—a service whose details a web controller does not need to care about (it only needs to know the interface).

Evolving code like this from the directory structure above is no easy task—where new code goes, how the directories are set up, and how things are named all require knowledge. Conway’s law tells us:

organizations which design systems … are constrained to produce designs which are copies of the communication structures of these organizations

In other words, a company’s technical architecture and design are influenced by its organizational structure. Likewise, Tyr’s law tells us:

A system’s software architecture and design are very closely related to that system’s directory structure.

In Phoenix 1.3, the biggest change is the change in directory structure. We are delighted to see that models no longer belong to web; indeed, models no longer even exist separately, and web exists merely as an appendage of the app. As a result, the web layer is dramatically thinned. When building a system, we no longer start from models, construct logic in controllers, and render in views; instead, we begin by considering how to build services, how to provide internal APIs, and then, on top of those APIs, provide a web interface.

Phoenix 1.3: Toward the Right Path

This lets us build systems with clearer logic:

Phoenix 1.3: Toward the Right Path

With such a directory structure, at a glance we can roughly tell what services the system provides and where each service’s boundaries lie:

Phoenix 1.3: Toward the Right Path

Support for Umbrella Projects

In Elixir, the umbrella project is my favorite. I not only like to divide services into spheres of influence through directories, but I am even fonder of structuring them as different apps to further define their boundaries at runtime. Application is a very important concept in the Erlang VM, one that does not appear in other VMs or languages. You can regard an Erlang VM as an operating system in which many applications, each with its own role, are running, and each application manages its own processes. In Rails, the logger is a module and the db connector is a module; they run in the context of the current code. In Elixir/Erlang, the logger is an app and the db connector is an app. When you want to log, you actually send a message to the logger app asking it to handle the log, and the final write of the log happens in a completely different context. I very, very much like this way of dividing a system into different apps to manage it at runtime. It makes system management simple, boundaries clear, decoupling easy, and the system’s overall anatomy clear at a glance.

In versions before Phoenix 1.2, one way I used Phoenix was to first create an umbrella project and then create a Phoenix app inside it containing only controllers and views; this was a bit awkward. In Phoenix 1.3, we can finally use Phoenix directly to create an umbrella project:

Phoenix 1.3: Toward the Right Path

This makes the example I mentioned in the article xxx much easier to build structurally:

Phoenix 1.3: Toward the Right Path

Engineers experienced in decoupling may disdain all of the above; but it does point out a clear structural path for less experienced engineers, especially the many who move directly from Rails to Phoenix and still have only a smattering of understanding of Elixir/Erlang VM. And giving the right direction structurally is often a framework’s greatest contribution. I am delighted that Phoenix 1.3 has finally taken this step.

Of course, taking such a step is painful, and it can easily pull a groin. Many excellent third-party libraries built on Phoenix suddenly become hard to use. When writing code, it is difficult to directly use existing libraries architected on Phoenix 1.2, so constraints spring up everywhere and one can only shuffle forward — and within the foreseeable few months, this situation will not change much. Yet this pain is unavoidable for an architecture to gradually mature: to leave the path of wholesale imitation, combine the language’s characteristics, and form its own distinctive approach.

Interestingly, Django, the first framework I used on a large scale, also completed its metamorphosis from function-based views to class-based views in its 1.2-to-1.3 upgrade. Could this be the wheel of fate coming full circle?

MORE IN Languages & Craft

Further Reading in This Column

View all essays in “Languages & Craft” →

Time to Meet jj: Git's Next Evolutionary Step

Git has ruled version control for twenty years, but its staging area, brittle rebases, and blocking merge conflicts are becoming liabilities in the AI era. jj rebuilds the mental model from the ground up — while staying fully Git-compatible. Here's why it deserves your attention.

New Hope (2025–2026)5,588 chars · 12 min read