ElixirConf 2017 - Phoenix after 100000 lines - Renan Ranelli
Given phoenix’s rise in popularity, the internet is flooded with examples and “getting started” tutorials.
What happens when your phoenix app grows beyond the “trivial” examples we see in the wild? What are the good patterns you should adopt and what are the ones you should avoid?
In this talk I’m going to share the problems (and solutions!) we encountered while growing our 2+ year old phoenix app beyond 100,000 lines of code, while also migrating from server-side html to a full fledged SPA.
After this talk, the listener will be able to (hopefully) better access the trade-offs and impacts on using Phoenix for their next (hopefully big) project.
The solutions presented seem to be: avoid unnecessary imports/requires, use Module.concat if you are sure you can get away with (probably best avoided I would assume), and avoid cyclic dependencies. The first seems easy enough, the second seems like its best to avoid, and the third seems like it requires changing your logic. Are cyclical dependencies intrinsically bad, or are they only bad because of the effect they have on compile time? In other words, should I be going out of my way to avoid them even before compile time becomes an issue?
I enjoyed seeing some of the problems that they came up against in such a large app. I definitely like how easy it is to create new types for Ecto, makes it very extensible. Wish he had some more information about how they manage that large a code base, module structure, file system structure, break down into separate library apps and umbrella applications.
I would say the Module.concat is best left for libraries and frameworks. So if you are writing a library that relies heavily on meta-programming, you may want to have that in mind but not in your app. At the time the article was written, which is indeed excellent, we got some of those lessons and applied them to Ecto (Phoenix already did it).
It is also worth mentioning Elixir v1.6 will improve the compile-time tracking so hopefully we can keep ahead of most of the community when it comes to compilation times.
I just finished the talk, I just want to clarify one more thing regarding the compilation. Renan mentioned that the Phoenix router takes a long time to compile but that’s likely not the case. The issue is that a lot of his application likely depends on the router and that holds everything else back. To some extent this is Phoenix’s fault since controllers and views import the router and that sets up a compile time dependency. I have pinged Renan to confirm this and opened up an issue on Phoenix’s issues tracker.
Just wanted to add that It’s pretty easy to introduce a lot of module dependencies to the router via plugs, e.g.:
def call(conn, _) do
this introduces a compile-time dependency between the Router and MyApp.CurrentUser
And then if the Users/User modules (or any of their runtime/compile dependencies) change the router will need to be recompiled and thus all controllers & views. Although it’s a hacky solution, having one Module.concat in critical place (e.g. the one plug that reaches out to schemas) can save a lot of recompilations.
IMHO cyclical deps are intrinsically bad most of the times, since when two modules depend from each other, they are tightly coupled (one does not work without the other and vice-versa).
But (and that’s a big but) sometimes you just want to separate tightly coupled logic from a module in two modules for organisation purposes, and I think that’s OK, but I would try to extract a module which does not depend on its source module.