Phoenix is not your application (unlike Rails)

Learn Phoenix LiveView is the comprehensive tutorial that teaches you everything you need to build a complex, realistic, fully-featured web app with Phoenix LiveView. Click here to learn more!

Back when Ruby on Rails development was my day job, I can remember the Rails community being locked in constant debates about architecture. As a Rails codebase gets bigger, many developers felt that the basic model-view-controller pattern didn’t provide enough guidance, and it was[1] common to invent new abstractions like “service objects” or “form objects” to help untangle the complexity.

No-one could agree on the best approach, but one of the more popular mantras was that “Rails is not your application”. Ruby on Rails, goes this theory, is a web framework, and the technicalities of the web - stuff like HTTP requests, cookies, routing, etc. - are orthogonal to your business logic. Therefore, if you want to break your complex codebase into manageable, loosely-coupled components, you could try this split:

  • A Rails-based layer handling web-specific logic, such as “return a HTTP 200 response with HTML that renders information about a product.”
  • A Ruby-based “service layer” handling your actual business logic, such as “place a new order with the product’s supplier when our inventory drops below 1,000 units.”

You’re not creating a Rails application, you’re creating a Ruby application that happens to use Rails for its web layer. Because Rails is not your application. Geddit?

This idea received light pushback from a minor figure in the Rails space:

But whatever that guy thinks, my bigger problem with “Rails is not your application” is that I could never actually make it work. The different components of Rails - ActiveRecord, ActionView, ActionController, etc. - are so tightly intertwined that if you need to load one of them, you’re inevitably going to load them all. And since your “service layer” will all but certainly make heavy use of ActiveRecord, it’s going to end up coupled to every other part of the Rails stack too, often in subtle or implicit ways that I was never able to untangle.

I don’t say this to knock on the Rails team. They designed Rails with certain priorities in mind, and “make it easy to decouple your web layer from your non-web business logic” wasn’t one of them, which is fine. It wouldn’t be fair to criticise DHH et al. for failing to achieve a goal that they weren’t even aiming for.

But the fact remains: Rails is your application. You can try to treat it otherwise, but you’ll be fighting uphill.

Learn Phoenix fast

Phoenix on Rails is a 72-lesson tutorial on web development with Elixir, Phoenix and LiveView, for programmers who already know Ruby on Rails.

Get part 1 for free:

How Phoenix is different

When I switched to Phoenix as my main stack, I was quickly reminded of that old debate from Rails land - because Phoenix takes the polar opposite tack.

Just look at the default structure of a newly-generated Phoenix app, which contains top-level directories lib, test, assets, priv and config:

$ mix phx.new arrowsmith
$ ls arrowsmith/
README.md assets    config    lib       mix.exs   priv      test

The most important one is lib, which is where you’ll put all of your main application code, similar to Rails’s app directory. By default, it’s divided in two:

$ cd arrowsmith
$ ls lib
arrowsmith        arrowsmith.ex     arrowsmith_web    arrowsmith_web.ex

We have two subdirectories in which to put our application code[2]: arrowsmith and arrowsmith_web. And if you’ve been paying attention this far, you can probably guess how it works:

  • lib/arrowsmith_web handles only the things that are specifically related to the web, such as parsing incoming HTTP requests and rendering responses with HTML or JSON.
  • lib/arrowsmith contains the app’s core business logic, separate to the web layer.

But we’re still building a Phoenix app, right? Our starter app makes use of various Phoenix modules like Phoenix.Controller and Phoenix.Router, but it’s interesting to note where it uses them:

$ grep -ril phoenix lib
lib/arrowsmith/application.ex
lib/arrowsmith_web/telemetry.ex
lib/arrowsmith_web/router.ex
lib/arrowsmith_web/components/core_components.ex
lib/arrowsmith_web/components/layouts/root.html.heex
lib/arrowsmith_web/components/layouts/app.html.heex
lib/arrowsmith_web/endpoint.ex
lib/arrowsmith_web/controllers/error_json.ex
lib/arrowsmith_web/controllers/page_html/home.html.heex
lib/arrowsmith_web/controllers/error_html.ex
lib/arrowsmith_web.ex

As you can see, all references to “Phoenix” but one occur within arrowsmith_web, the web layer.

Look inside the one exception, lib/arrowsmith/application.ex, and you’ll see its only mention of “Phoenix” (outside of a comment) is to set up Phoenix.PubSub, a tool for communication between Elixir processes that’s not directly related to any web stuff:

# lib/arrowsmith/application.ex
defmodule Arrowsmith.Application do
  

  @impl true
  def start(_type, _args) do
    children = [
      
      {Phoenix.PubSub, name: Arrowsmith.PubSub},
    ]

    
  end

  
end

Similarly, if you generate scaffolded code inside this app using Phoenix commands like mix phx.gen.html or mix phx.gen.schema, you might see new LoC within lib/arrowsmith, but they won’t include the word “Phoenix”. Why would they need to?

Phoenix is, first and foremost, a web library. You use Phoenix to build an HTTP server that acts as an interface between your app and the outside world. For the underlying business domain - the stuff that would be the same no matter what the UI - Phoenix is incidental. Elixir alone is enough to solve your problems.

In Rails you build a Rails application. In Phoenix you build an Elixir application that happens to use Phoenix for its web layer, with a clean, logical, and loosely-coupled separation of concerns. Whether or not that’s better than the approach preferred by Rails, I’ll let you decide.

Phoenix is not your application. And the real magic begins once you realise that your “Phoenix app” is really an OTP app, with access to the full power of the Erlang virtual machine. But that’s a topic for a future post.