The Magic of Rails: exploring the principles and techniques behind the framework
speakerdeck.com
speakerdeck.com
A lot of big companies use Rails and are successful at a large human scale. But one thing that is often not talked about is that they have the financial resources to hire the very best programmers in the world, and often they hire the very people who build the framework (e.g. Shopify, GitHub I think had a few members go through it, etc).
This is critical when the biggest questions with Rails is: how to achieve clean code, modularity and separation of concerns; how to achieve high throughput.
It doesn't surprise me that Basecamp says they never have issues with a huge monolith in Rails: they invented the framework and know their way around it. It doesn't surprise me that Shopify doesn't have issues with scalability: they have Rails core team members and Ruby engineers dedicated to creating an entire new Ruby VM.
If the problems with Rails are solved by "throw the best people in the world at it" then it's a self-limited proposition. I have worked on countless Rails apps over the years, not one would pass the "this is just great code" test. They were not built by incompetent programmers. It's just that the "Rails way" needs a very heavy guiding hand behind it. In the real world that is often easy to lose: senior team members leave, you don't have time to rewrite parts of the code, you may not even have the expertise in-house to improve the codebase significantly. Bigger companies don't usually have these problems.
And if you look at big Open Source Rails projects (Discourse, Spree, Gitlab), you'll often see the same patterns: 1000+ line models, non-standard Rails-way ideas like services and operators, hundreds of files all in the same place without any clear modularity... and these are not bad programmers at all. Maybe Rails at human scale is just hard, or maybe there’s something about the framework that leads to this.
Notice the cliche "Rails is so much more than a framework."
The same magic that makes one so productive at the beginning (start-up stage) is what comes back to bite you in the ass when the project grows into an actual product; unless you are careful and willing to trade off speed-to-market, which isn't what you want to do with a startup.
Catch-22 because the app has gone off the rails by the time you are successful, and everyone says: "Oh, you shouldn't have done that."
That's why you have all these different tools and concepts; since they try to sandpaper over all the magical complexity by adding more complexity, it becomes a no-true Scotsman trivia game.
What trips me up tbh is how tech history repeats itself almost verbatim. The Smalltalk story is very similar but with a different cast.
If you remove friction, people will build bigger foot guns. If you install too many guardrails, holding it right requires contortion.
The point is not which set of tools you use. There is no substitute for understanding the problem domain, the tools at hand and the labor market you have access to. Everything has a cost, you can't get free advantages from some technical insight or another.
I've built a video course platform now in Flask, Phoenix and Rails and I've built about 60% of the platform in all 3 tech stacks. The Rails app has ~7k lines of code (LOC count includes everything but tests), it's substantially less than the others. The Phoenix app was by far the worst of the 3 in terms of how much code I had to write and overall "difficulty" of the code. The Flask app sat somewhere in the middle, at times it made doing certain things easier and more efficient than Rails but if I had to rank overall developer happiness and how easy it was to do things it would be Rails > Flask > Phoenix where let's say Rails is a 8.5 in terms of happiness, Flask is a solid 7 and Phoenix was maybe a 4 or 5. This includes a number of real-time elements like having a persistent video player that survives across page navigations, notifications and comments being broadcast to all connected clients with authentication and more.
What exactly were the painpoints?
My impression was Elixir / Phoenix was ranking high in developer happiness.
Elixir isn't really in the same ballpark as Python and Ruby for package support. Lots of 3rd party vendors don't have official SDKs for Elixir and having to recreate certain components of those SDKs manually can be very tricky. This is especially true for payment gateways like Stripe, Payal, Paddle and others since there's gory details about certain things aren't as easy as making an HTTP call with a token. So you end up having to recreate these things from scratch because most community versions are abandoned due to them being maintained by 1 or 2 people who stopped using it.
Also, LiveView's APIs keep changing. The amount of churn was out of control for a few years where it felt like every few months you had to make drastic rewrites to keep up with it all. At the same time the docs are very minimal and community support through blog posts weren't there in the same way they are for Rails and Flask. It felt like you were on your own to solve everything in a fast moving world with an ever growing API.
Then for the language, Elixir didn't click for me, even after writing 10k lines of code. I felt like I could get pretty far and understood enough to get most things done but I felt like I was getting stonewall'd quite frequently and couldn't solve certain basic problems after exhausting all pre-existing resources (my brain, docs, blog posts, open source projects, etc.). I don't feel I have this same issue in Python or Ruby. There's such a big community that nearly everything can be found, or at least get you to the point where you can apply something to your domain without asking in chat for help.
A good example is https://github.com/josevalim/nested-map-reduce-traversal which is something Jose and I worked on a few years ago. If he didn't step in and provide an example on how to do what's essentially a nested loop while modifying a counter in Elixir I would have had to stop developing my project. I still have to step back and spend a full 5 minutes trying to mentally parse his solution where as the Python and Ruby solutions are immediate with no effort.
When trying to build a pretty big project, these routine blockers or having to spend 3 hours (or 3 days) down a rabbit hole vs just doing it in a few minutes make a big difference -- this is also after spending months learning the language and ecosystem already. You end up getting fatigued from having to build so many libraries to get to the point where you can work on your main's app goal and it's demoralizing getting hard blocked. There's also this pit in your stomach that's concerned at how quickly you can get stuck in a way that you can't self-resolve. When trying to build a business, these are all very dangerous things.
You put all of that together and my happiness level for Elixir was kind of low.
It's nothing personal, just my opinion. I think it has good ideas and concepts in places but it didn't feel like the juice was worth the squeeze for someone like me who didn't already have many years of functional programming experience beforehand. Flask and Rails are both fast enough where you can build a million dollar a year business on a single $40 / month server with tens of thousands of customers. You can still get p95+ <= 100ms response times from your back-end in this scenario and both tech stacks have a good story for using websockets nowadays. Thankfully Hotwire Turbo is back-end agnostic which means you can build really nice feeling front-ends with any tech stack with minimal JS. I'm really happy DHH pioneered this pattern with Turbolinks which evolved into something wonderful.
Somehow makes me think for this web stuff just use a well established glue language / toolkit with a huge and thriving community, e.g. like you mentioned RoR, Flask or even Laravel ;) - which lets you focus more on building your solution for your business problem.
And for certain things where it's niche, e.g. you need real time with hundreds of thousands of nodes, self healing etc features, use Elixir with BEAM.
Yep your comment sums it up well. Although Kubernetes does allow you to horizontally scale decently well with any tech stack. I've run a few Flask + Celery apps (replace that with Rails + Sidekiq or whatever your preferred stack is) on Kubernetes and it's working out quite nicely. Zero downtime deploys (and even node upgrades) with a deployment model that makes sense for individuals and large teams (gitops philosophy using well supported tools like Argo CD, etc.).
For things that don't need that scale, a single server with Docker Compose does the trick. I've been doing that for about 8 years now with Docker and Ansible. ~20 lines of YAML configuration to get a machine from ground zero to fully up and running, secured and serving any Dockerized app of your choosing over a custom domain with SSL with automated DB backups, monitoring, logging, etc..
Rails apps are way over represented among large web-based businesses. Shopify, GitHub, Gusto, Airbnb, etc. There are countless examples of Rails scaling beyond what anyone could reasonably expect their web app to need.
It's not that it can't scale; it's just that it's too easy to make a mess in the way, which would prevent you from doing so.
So what ends up happening is that you pay for mistakes or someone else's, but at the end of the day, you are still on your own.
The Ruby/Rails at Shopify is much more sane (by my definition) looking much more like regular API made using a statically typed language (lots of Sorbet type info). There is of course dynamic magic, but tends to be limited rather than preferred, and where feasible static Sorbet type info is generated to match what the magic produces so users of magic can operate as if it was just a static typed library with a bit of DSL. There's enough in-house specifics that I wouldn't even call it Ruby on Rails, but rather Shopify Ruby/Rails and it's constantly evolving. There can be 3 ways of doing something: the old way (being phased out), the current way, and perhaps a new one being tested out in some part of the codebase.
Shopify has built massive tooling (https://shopify.engineering/shopify-made-patterns-in-our-rai...). Github was stuck on an old version of Rails for years due to the amount of customization they had done. (It was actually a big deal when they finally were able to run on the latest version: https://github.blog/2018-09-28-upgrading-github-from-rails-3...)
I am a bit over >1 year with Rails and I don't see this to be the case. There is some "group think" perhaps, but every ecosystem has that (Whats the "pythonic" way or Java way to write x). I work at Shopify which uses Ruby and Rails extensively, and I don't think things have "gone off the rails". We use other technologies where appropriate, but for the core business logic cases, Rails is fantastic.
That's why I think the argument is a weak appeal to authority. Throw enough money at a problem, and you can scale anything involving computers. Notice how expensive is Heroku.
But you can't add more team members to a project and expect things to go faster, which is where the real problem with Rails lies.
The place I currently work has a 10 year old Rails monolith, less than 50 developers (I joined at ~10 devs) and features are still shipped very quickly. I would say that the Rails framework is one aspect of several that has allowed the company to ship fast and be very profitable without having to grow the engineering team massively.
I've worked places where we've made a complete mess out of Java or .NET code. You might be correct that there is a degree of group think within Rails developers, but that's actually a good thing because they don't deviate too far away from a set of conventions for writing their code, which causes less mess. And they at least know what a convention is and stick to it once you agree on it.
At one point in my career I thought I had put Rails behind me because it's not that compatible with services that are supposed to be more scalable. However, this again goes against your argument, I saw just how much overhead was being generated by that kind of architecture. Yes you could have way more developers working on the product, but they weren't working on deliverable features, they were working on integrating with the rest of the system. I'm sure there are team scalable limits to Rails. I've noticed though that as our product has become more complex more of the code needs to be frontend in order to support that interface complexity and so perhaps that team limit can actually be stretched out a very long way.
Caveats: I think Rails is good and I'm defending it because I think the argument made here is bunk, but I'm sure there are Rails codebases that are a complete mess. I've pointed out other languages where I've seen issues, but I've also seen great codebases in those languages, so it's not a knock against them in particular. I'm not opposed or for any particular architecture, my experience is just that something like Rails can get you started much faster and can scale much further than a subset of devs seem to think.
Rails can work well but it’s going to be expensive. Everything is a trade-off.
Because Devs don’t see operating costs, they don’t feel the pain.
Anyway both of our opinions are based on anecdote. It would be interesting to see an analysis on real data but I wouldn't trust that anyone could get that data from industry, correctly accounting for failures.
I don’t think they have anything to do at all with the stack. I think they’re more related to the people, the working culture, the practices and in particular the rotation and the “happiness” within the team
https://shopify.engineering/shopify-made-patterns-in-our-rai...
"Going off the rails" isn't an indication that Rails is bad, as much as a claim that it can't fit the needs of a company with either splitting to other technologies ("We use other technologies where appropriate") or painting outside of the Rails lines.
Like, I am the first person to be pessimistic about a framework or language, but I've been working with both Ruby and Rails since 2007. Every single company that has strayed outside the defaults in the name of 'making a scalable app' has spent tons of money in tech debt, maintenance, etc. Yet the companies that stayed on the tracks had very few issues with scalability or tech debt.
I think pretty much all developers seem to forget that when you bring in a third party library or a unique pattern, you also inherit maintenance in both those areas.
The problem isn't the language or framework, the problem is that developers try to solve problems by deviating from tested and proven methods. "Convention over configuration" is what Rails is built around. Why do so many people prefer configuration over convention? Why not just pick another framework, or possibly even language if you are going to do this?
Note that I'm not affiliated with Rails, companies financially affiliated with Ruby and/or the Rails framework, and have no desire to be. I just like building awesome stuff.
There is your example.
It’s not the tools what make things a mess or a great piece of art. It’s the people using it.
One of the reasons why I hate Rails is the steep learning curve. It's really easy to get something out the door with Rails. Whether or not it's maintainable long-term is what's up for debate. There are often 10 ways to get the same result: 1-5 are really bad, 6 and 7 are acceptable, 8 is good, 9 is great, 10 is perfect. Not everything has to be 10, or even 9, or even 8; the fewer 1-5s you have the better.
The official "Getting Started" guides don't help this at all. Granted, they are indeed marketed correctly: they are for getting started. But it lends to this idea that that all Rails applications have to look that way — the "getting started" way.
Many of my applications (and many of the shops I'm brought in to "better") end up shipping with more than what comes out of the box. This might sound normal to folk on HN — engineers are supposed to engineer things, after all — but it's lost on many teams and orgs. ("Many teams and orgs" are largely teams and orgs we have never heard about, and there are plenty of them.)
Taking a peak at my usual interface stack includes commands, components, decorators, forms, inputs, queries, presenters, serializers, and services. This is in addition to the default controllers, helpers, mailers, models, views.
My ultimate takeaway from seeing over a two dozen "this Rails app is painful to use" situations in the last two years is that the developers hadn't extended past the original patterns that were given to them when they originally spun up the app. This largely results in losing a lot of the Rails magic because developers are (unintentionally wrongfully) forced to do things that goes against Rails' expectations.
There are other things besides shipping better abstractions too, obviously. The one that stands out the most is controllers doing too much and becoming God controllers; developers get lazy (or just aren't aware) and simply define routes that are RESTful in the router, but aren't in the actual controller definition. This makes for a tangled mess.
There's a way to make Rails as much of a joy to use as it is when you first hit `rails new` — it just takes some experience and diligence. That can be said for every framework. But what can't be said for every other framework is that Rails makes it way too easy to write really bad Ruby code that, ultimately, gets you the "right" result. Avoiding those is the hard part.
(I'm currently looking for work in case anyone wants to talk)
but TBH, discipline + correct factoring is kinda the baseline to produce decent systems.
It's still leagues better with django/rails than with "microframeworks" which are basically a giant spool of rope for anything more than a handful of endpoints
is there a framework that doesn't??
Rails comes with a lot of the patterns I implement out of the box. Forms are just ActiveModel objects. Services are just POROs, as are queries. A decorator pattern can be done in 25 lines of code (https://gist.github.com/joshmn/87f14dc7d6b45a72bd4e3f4fb8d82... Commands are a bit more difficult but anything that can respond to context with flow control handling is easy to grep.
There's a fine line between implementing patterns and implementing patterns that behave as Rails expects them to. Thankfully, as mentioned in Eileen's talk, Rails makes it really easy to do this. While the underlying logic may not be documented on the Rails Getting Started guides, peaking under the hood isn't as intimidating as one would think.
Would love to hear a short bit on what each of these does in your Rails codebases! I'm familiar with decorators, queries, presenters, serializers, services, but commands, components, forms, and inputs are new to me. Would be great to see what I'm missing out on.
Commands — I usually will ship with Rectify https://github.com/andypike/rectify. It's a great layer for my controller to handle request contexts. You might think this is a glorified service object but I'm not a fan of service objects unless they do just one thing (and are allowed to loudly fail).
Components — https://viewcomponent.org/ I'm not incredibly sold on them but they do clean things up.
Forms — Rectify has this shipped but it uses Virtus which is quite old and hasn't been touched in many years. Shipping my own similar is usually something like https://gist.github.com/joshmn/39eb6f302650e09743387710591ab.... Not all of my form attributes will map directly to a model, but I want my form helpers to actually help me so I need some sort of object that _can_.
Inputs — I'll extend SimpleForm inputs (something I can't live without) to DRY up my forms.
All together, this is typically something like this (the form and command pattern, at least): https://gist.github.com/joshmn/65cfbccc6c64abe6d463ba9708b00... (not tested)
There's usually also an `admin` directory since I also usually ship with ActiveAdmin or Trestle. I've fallen in love with the latter after making it behave more like ActiveAdmin. When I need to drop out of its usual constructs I'll just write a custom controller, but for the majority of administrative stuff I can get 95% of the way there with the DSL of either/or, freeing up time for domain-specific problem solving.
Some nuggets here: https://news.ycombinator.com/item?id=35755423
A lot of it has been trying and failing — a lot of times, across a lot of different applications. Many of which have been my own, but as of the last few years there have been other people's applications too.
The first thing I look for when I go into someone else's Rails app is to grep through `routes.rb` and look for every route drawn that's not `resource` or `resources`. This has proven to be a good indicator of the amount of smells I'll encounter in the application. You can draw every route with `resources` or `resource` and get clean controllers, which leads me to my next point:
Your controllers are the most important part of your application. It's the first and last thing your user touches. Getting them right is the most difficult part of architecting any application. "Fat models skinny controllers" does a disservice to models, though, because they shouldn't be fat, (insert transition)...
Models should represent your database and nothing more.
I could go on and on, and am happy to, but I'll quickly be writing a mini-book here and detract from Eileen's great talk at Railsconf. :)
The good thing is there's value delivered quickly essential to growing the company, but amounts to a lot tech debt and human scaling problems a few years later.
for whatever reasons, rails became easy to discover and use.
that so many ended up committing git based crimes with it is not rails fault.
personally i blame education and media. we can do better.
Unless you say something specific, then you're doing twitter-like pile on and nothing of substance is being added by your voice.
Here's my critique of rails that offers specifics.
There are parts of the framework that are automatically baked in but unknown and unused by the majority of projects. Message encryptor comes to mind. I'd prefer to choose a feature set, like a toggle config. Why should my company pay for objects to reside in memory that have never and will never be used?
Or how about the need to duplicate directory structure within M, V, and/or C? Kinda exhausting and boring. It doesn't take long until the question arises, why does it have to be that way? Oh, well, of course it doesn't. But if you're doing it "the rails way" where its convention over configuration then it should be done. Not to mention well known widely used rubygems rely on following the convention withing your own app. I'm not saying its bad, but its boilerplate the generators currently don't handle.
Aside from rails, I see alot more complexity added, not by rails or follwong rails convention, but by adhd bored devs that play social games, that are a weird mix of "I'm better than you" and "I'm tragically insecure of you finding out I'm not as so smart". I'll take a team of middling hard working devs over a 10xer on the spectrum any day.
Spooky.
Are you saying the phrasing I just used matched verbatim?
Tada!
With Rails, you can call `relation.map(&:id)` and `relation.pluck(:id)` and get the same thing — an array of `id`. But using map is 50x slower because it loads the entire model into memory (ActiveModel is very expensive) before enumerating; pluck fires a query and hits the result set from ActiveRecord — very inexpensive!
Average ability participants then fall for this foot gun (and others like it) and end up designing things poorly. One overlook quickly turns into accelerant at scale. This isn't unique to Rails, but Ruby — the delight that it is — makes it almost too trivial to do.
I've seen this happen at Rails shops many of us have an account on, where I wouldn't consider the people I worked with average ability participants either.
Which is to say, there’s a lot of ways to do things, and trade offs between the different ways, but isn’t that the case with any library or framework?
Sure – if – that's fine and optimal! In larger apps, where there's more complexity, there are so many paths that aren't that trivial and that's where the trouble begins.
You can do a similar performance issue with anything: here is a raw example: give to anyone a non-ORM framework and the chances of someone hitting multiple times the DB to query the same records increases with team size and app scope. I am not sure there is a framework that can protect your colleagues from doing select * and then count the in memory objects instead of doing a count on DB.
As far as Rails goes, for all it's shortcomings, people forget what web development was like in 2003. The dominant paradigms were overwrought XML-powered J2EE with incredibly low power-to-weight ratio for web development, or unstructured PHP wild west stuff. These days every language has a framework that was heavily influenced (directly or indirectly) by Rails. Sure I wouldn't use Rails everywhere (1000+ engineer team: java, lots of concurrency: elixir/erlang, lower-level large systems: go/rust, etc), but it still has a great sweet spot from the prototype to moderate sized web app / API. Things that become weaknesses as you scale (eg. ActiveRecord pattern) are based on contextual tradeoffs that need to be made thoughtfully versus declaring them table stakes for all web frameworks.
Monoliths are fun to bash, but I'd rather have a monolith than poorly done microservices.
The problem is that Ruby encourages (or, perhaps, doesn't discourage) a certain kind of tangled monolith.