In 2014, the Phoenix framework emerged onto the web development scene as a fast, productive, and concurrent solution for the modern web. Its focus was on massive concurrency, thanks to the power of the Elixir language and Erlang's BEAM virtual machine. The original branding stressed the productive aspect, positioning the framework against the venerable king of productivity, Ruby on Rails. Those who are familiar with Rails will see a lot of inspiration from Rails. The way Phoenix auto-reloads code in development mode makes it just as easy to use as Rails. The router grammar requires a good squint to see the difference between the two. This inspiration certainly makes for a quick learning experience for Rails developers. But I wonder: at what cost? Does Phoenix make it too easy to structure your application like a Rails application? Is this convenience to the detriment of using the full power and expressivity of Elixir?
Originally, the core team clearly optimized Phoenix for productivity and similarity to Rails. There were modules that were called "models" that you were encouraged to treat similarly to Rails models. Concerns were mixed in the model modules (say that three times fast) between querying, persistence, and business logic. The first application that I spiked out in these days looked a lot like a Rails application, despite reading through Programming Phoenix. I understood that I should start to split business logic out into separate applications and only use Phoenix as the web interface to the application. However, the umbrella application concept put me into an "architecture astronaut" mode and I could either ignore it and "be productive" or endlessly spin my wheels.
I admit, many of my problems in Phoenix prior to the 1.3 release likely stemmed from my simultaneously learning Elixir and Phoenix; I didn't know enough of either to be very effective with the toolset. Compounded by the fact that I was only using it on side projects, I wanted to continue to build and grow my skills rather than take the time to design my application as a piece of software first and a web interface second. I think this is often the same problem that people have early on in their Rails days: they want to build and "ship stuff," not deliberately build maintainable software.
With the release of v1.3.0, Phoenix radically shifted the way the default generators worked. In his Lonestar ElixirConf 2017 keynote, Chris McCord described the new default architecture for Phoenix applications, which revolved around contexts. Based in Domain-Driven Design, contexts (or, as people are loath to say, bounded contexts) give a place for your business logic to live and define the interface between different components of your system. Does your application have a system of accounts with many bits of logic around them? Okay, those likely belong in an
Accounts context. Are you interfacing with social media as a facet of your business? Great, you probably should have a
Contexts are a great first step toward wrangling the architecture of a Phoenix application. Instead of a Phoenix-is-your-architecture structure, you can move toward a semblance of loosely connected pieces that work together. These form natural boundaries between the components of your application. The boundaries can later be used as seams by which you can slice-and-dice your application into several smaller pieces, should that be something that you need to do. The Phoenix maintainers made the decision to change the default generators to use this design pattern for structuring your application and it was a very positive move. However, the part that I worry about is that they chose a specific piece of the default Phoenix stack as the interface between different contexts: Ecto, the database Domain-Specific Language (DSL) and interface language.
Because of the decision to use Ecto as the boundary between generated contexts, that is the natural direction that people will take when learning Phoenix. Ecto is a wonderful library and very fun to work with. It helps you to be more thoughtful about your database design. You have to design queries since you don't get a lot for "free" like in Rails' ActiveRecord. Ecto also removes many of the ways that you can accidentally hamstring yourself (I'm looking at you, N+1 queries!). Its thoughtful architecture makes you be explicit about what you want from a query. However, because Ecto is a database library, that means that the coordination of work between your contexts is fundamentally coupled to interacting with a database.
There is nothing inherently wrong with this decision. If you are making a simple CRUD application, this pattern will serve you well and will help you move quickly. However, how many "simple CRUD applications" continue to stay simple? How many of them continue to be CRUD applications? Every web app that I've ever worked on has increasingly gravitated away from the happy place where you are building a "glorified spreadsheet". As time goes on, more and more of your business logic has nothing to do with CRUD and has everything to do with munging that data. The business logic is happier without knowing it works with a database.
This mild existential crisis that I had with the architecture of my Elixir/Phoenix application was spurred by reading an excellent commentary on building a Multi-User Dungeon (MUD) in Erlang and the thought and care that the author put into designing a process-based architecture for the application. It made me wonder: is Ecto really the best choice for the interface "language" between contexts? Is there something else we should use that gives us the nice affordances of
Ecto.Changesets and their validation, but doesn't intrinsically tie us to the database?
I have considered using Formex as the mediation point between contexts. It interfaces well with
Phoenix.HTML and is pretty lightweight in what you need to know to use it for interacting with the application. I have also considered rolling my own way of doing things, but that usually isn't the wise choice. I'm wondering if anyone else out there has thought about this? Do you have any way of breaking the tie between your traditional database and your application? I'd love to hear from you in the comments!