When Should You Not Use Rails?
codefol.io
codefol.io
- All of our "lightweight Sinatra(and similar) API" services eventually start to look more and more like Rails apps. Rails does many small developer convenience things well. Which you do not notice until you build this lightweight API yourself. E.g. console, logging, migrations, database connection pooling, rspec integration, i18n.
- No one likes to work with your arbitrary personal project structure conventions. Where's the code? 'lib'? 'core'? 'app'? 'api'? Also, no one wants to learn your lightweight "data mapper" pattern written from scratch because you thought ActiveRecord is too bloated and "does not scale". That being said, there's quite a few things you can arbitrarily pick in Rails projects as well which others will find surprising. But the spectrum of those choices is little narrower.
- Developers most of the time assume database connection pooling just magically happens. Sysadmins do have no desire to debug your apps. After couple weeks of back and forth you may realise that Rails does database connection pooling for you. And simply requiring 'activerecord' and establishing connection in your Sinatra app does not.
- One day someone doing production maintenance wanted to remind themselves rake task name. And ran 'bundle exec rake' forgetting to add '-T'. Default task was rspec. It dropped production database. That day many learned that Rails has safeguards against things like this, while none of those "lightweight arbitrary structured APIs" had any. Though the lesson was clearly not very good, since we did this again couple years later.
While having a depth of defense is a good idea (and so "+1 to rspec for its default rake command not performing destructive operations" is ok), I'd suggest the bigger takeaway should've been about permissions surrounding the production database. -- You can't accidentally do something you don't have permission to do, or can't do easily.
Rails doesn't have any separate facility for migration account vs production account, by default. Which because our deployment paradigm also doesn't have any such separation, resulted in an awkward conversation with our DBAs where we explained that our production login requires the DDL permissions they thought should be reserved for an admin user.
I think they're right, but there's only one place for setting the value of DATABASE_USER in each environment. So do you handle migrations by hand, out of band with a different user than production uses? (How should a regular Heroku user handle this issue, I guess is where I'm looking for a straightforward answer to this common issue that I think you're absolutely right about...)
I suggest ideally all the maintenance tasks can be done automatically, without needing the command to be run manually. Such scripts can then run in staging before in prod; or if the scripts necessarily only apply to prod, they should be reviewed carefully.
If prod admin must be done manually, then make you could make it harder to run the development scripts with such admin privileges.
The principle of least required privilege seems to suggest that the production web server shouldn't have permission to perform DDL when it doesn't need it (so that fat-finger would have likely never happened, or never caused a problem, so long as it was only running in the context of the prod web service.)
It's just a little bit surprising that there don't seem to be any concrete solutions for this, I find it hard to believe I'm the first to whine about this issue related to commonly understood DBA best-practices. I don't know how you solve it, (maybe add a production_migrations environment?)
Worse, the CI/CD pipeline would break when I tried pulling the .env file out of the codebase. It took me a while to figure out why and finally keep all the config in the environment and not the codebase, I breathed a great sigh of relief when I was finally able to add .env to the gitignore.
Set DATABASE_USER and everything else to ENV['DATABASE_USER']. Better yet use connection strings instead, they're easier to manage. Devs need to hand-manage their own environment, I prefer direnv, but you can use dotenv if you have to, just dictate that .env has to be in the gitignore. Every single CI/CD platform and PaaS lets you set environment variables. So no .env files get pulled down on git pull, messing everyone's day up.
But is it documented anywhere? One of the Rails 6 bounties is for multiple database configurations to be made clear, IOW in configurations involving a partitioned database cluster, or a cluster with separate main and secondary streaming replicas for read and write locking separately; those configurations are all being made clearer in the latest Rails guides, as I understand it. There were always people that knew the right way to do it, but there was no one blessed right way to do it.
This seems like it belongs in the rails guides as a regular part of the instruction manual. Then again .env support itself is still separately provided by add-on gems, so maybe not. (Thanks for sharing!)
Writing your migrations in anything but SQL is one problem. (OK, perhaps pl/pgsql too.) Running it with a system user and database role that has access to the web app code is another.
But the major one is that web apps should use very limited database roles that only have execute permission for specific stored procedures. In any environment.
Anything else will lead to security exposures, as we continually see.
Yes, it takes longer and requires you to become proficient in SQL. So what? You might learn to make better use of your RDBMS. If you're working with data, you need to know SQL. And you especially need to know the access control features of your RDBMS.
From the perspective of data security, the answer to "when should you not use Rails?" is always.
Creating and destroying databases happens so sparingly, it can be done by some "root" user that is allowed to create the other users.
Granted that ot won't fix the problem if the next operation is to recreate all tables.
I'd argue the main question is: how is possible the test suite runs on the same db as the application? Even for local development it's a nightmare.
Honestly for anything of real-world size, that question is not well answered by rails either. Developers tend to stuff everything into either models or controllers until they learn about "service classes" and then they put all logic into service classes, with the same issues.
"Our models are too fat, let's refactor."
"Where should the code go?"
"Let's put them in a utility class."
"No, a utility class is a code smell from the point of view of true Object Oriented theory."
"Then let's put them in a service class."
"No, a service class is just a utility class with a different name."
"Then where should the code go?"
A long debate ensues, and then we end up with some crazy solution that includes everything that jeff_vader just suggested as a problematic:
" Where's the code? 'lib'? 'core'? 'app'? 'api'?"
There seems to be a real adversity among many to go ahead and create new classes as needed especially when that new class isn't obviously a model, view or a controller.
I just went over a short YouTube series DHH (creator of Rails) did where he walked through the production code behind Basecamp and I was struck by how different it looked to almost any other Rails app I recalled looking at previously.
But it seemed like there was not a whole lot that they were doing that would seem in any way out of place from a SOLID OO perspective. It's a very interesting set of videos to take a look at what a modern Rails app with a decent amount of complexity looks like as intended by the team who wrote the framework. You can find it here if you're interested https://www.youtube.com/playlist?list=PL3m89j0mV0pdNAg6x9oq6...
Fortunately, for whatever you decide, you can create as many folders as you want in `app/` and Rails will detect & autoload them for you.
There are so many things that are required in a modern web app that these "batteries included" frameworks provide for you that it just makes sense to use these for most standard projects.
I personally use Spring Boot as my default stack because just setting up a basic boot starter project gets me:
* Routing
* Server side templates
* i18n translation files
* database connection pooling
* database migrations
* ORM
* Logging
* Json serialization
* unit testing, integration testing, mocks
* CSRF
* Validation framework
* Dependency injection
* SMTP email sending
and much more, all compatible with each other
If I would start with a blank Sinatra/Express project I would have to piece together all of these myself from various libraries and my own code.
Can you give a specific example of when this poses a problem?
Rails can do many things but no framework can design a product’s data model, and when you have an excellent one Rails will sing.
Also sometimes you need some separate services for GPU/security/reasons. That’s fine.
I agree that if you use Rails and constantly override its conventions, you will have a bad day. But following conventions is usually a strength, not a weakness. Most websites are not shocking cutting-edge things that have never done before, but relatively boring simple tasks that involve grabbing data from databases and showing them to users via templates. Every decision you have to make is a cost, and if you have a small team, having basic naming convention decisions and directory location decisions already made accelerates development. There's a big advantage to following conventions when you want others to work with you, because different Rails applications look quite similar in many ways.
I have not had trouble overriding Rails when I needed to override something specific.
Btw, I like the original article. There's nothing that's good for all cases, so it's very important to understand what something is good and not good at.
Databases can have various constraints (uniqueness, etc.) enforced at the DB level. ActiveRecord also allows for uniqueness checks, but these are separate mechanisms. If you want to validate for uniqueness, two processes each running the same Rails app might concurrently check for uniqueness, assume validity, and then independently insert two non-unique records. At this point, your DB will throw an error. (Edit: Rails actually wraps this exception now, but if you follow the idiomatic pattern and don’t bother catching it, it just 500’s the request.) Rails is (edit: still) not wired to treat this as a uniqueness violation at all; it just has its own uniqueness validation mechanism that doesn’t even work in the most common use case for Rails.
In a concurrent environment like this with a shared DB, the only way to avoid check-and-set conditions is to just try the insert and only complain about uniqueness violations when the insert fails the DB-level uniqueness check. (Edit: Rails provides a wrapped exception for this use case, but that’s certainly not the conventional Rails way of handling uniqueness validations.)
Conventions are fine when those conventions work and actually enforce good practices. ActiveRecord in particular falls short of this. And without ActiveRecord, the rest of Rails doesn’t do much to distinguish itself from alternatives. It’s fine, but it’s not necessarily anything special.
ActiveRecord has thrown ActiveRecord::RecordNotUnique for this and done the right thing for over 11 years (the exception definition got moved to a different file at that point so idk exactly how old it is.)
> ActiveRecord in particular falls short of this
> without ActiveRecord, the rest of Rails doesn’t do much to distinguish itself from alternatives.
Callbacks are pretty bad but it sounds like Rails went through several near-complete rewrites worth of changes since the last time you used it. There's a lot more in Rails 6 compared to Rails 2.
I had these issues in Rails 3 when that was the newest version. I found the git blame you’re referring to here and it may have been written 11 years ago. Odd. It looks like maybe Rails 4 finally fixed this?
More to the point, the broken uniqueness validation is still there, as is the idiomatic “validate then write to the DB” race condition that leads to this issue in the first place. There’s zero correlation between stating in your ActiveRecord model that you would like uniqueness (or any other constraint) and actually enforcing that constraint in the database, where it actually works. Having an actual wrapped exception to catch is an improvement but it doesn’t fix the problem.
"This [uniqueness] helper validates that the attribute's value is unique right before the object gets saved. It does not create a uniqueness constraint in the database, so it may happen that two different database connections create two records with the same value for a column that you intend to be unique. To avoid that, you must create a unique index on that column in your database."
It's pretty clear from that text that if you want a unique index in a database, it has to be a uniqueness constraint in database itself. But that's true for any such system, that's not specific to Rails.
In most applications there are simple isolated validations you can verify without consulting the database, and other validations that can only be validated by consulting the database (such as uniqueness). Rails can do that, as can many other frameworks. I don't think that's broken, that's how things are.
Simply removing the uniqueness validation and relying exclusively on database indices would be strictly better. ActiveRecord dynamically infers from the DB schema which columns your tables have; it could also infer foreign key relationships and uniqueness guarantees and use them to generate pre-insertion “best guesses” about validity if the user wants to run those without necessarily inserting. But Rails is supposed to be an opinionated framework, and “just try to insert and we’ll let you know how that worked out for you, since we can’t make any guarantees otherwise” is a perfectly valid opinion.
No, you don't, because then you'll be able to lean on Rails' biggest strength, Ruby. You don't know how many times I've been able to just dig into a gem's or even Rails itself, find out what's going on, and simply write what I need leveraging all the existing code.
On the other hand, when we were replatforming our Rails project into Node for no good reason, we wound up having to rewrite a ton of stuff because, well, Ruby is a great server language, and Javascript is only at best a mediocre scripting language, and ES6 is only a marginal improvement.
Sometimes people don't like to use Django because they write fairly stateless services not involving a database, its understandable but that aspect "doesn't eat much hay" tbh.
As a middle ground I often recommend https://trypyramid.com/ which has more structure, a better plugin-integration infrastructure than flask but isn't so much bound to a SQL ORM (while you still can use sqlalchemy just fine).
To be frank django has been my go-to for almost every app I've built or prototyped which required a db, sometimes if exposing a REST API is more of the main goal I'll use DRF and then build a minimal frontend SPA using React (served by a separate app within the project). Not sure why it's not taught/used more tbh.
Some examples from crufty Java world. You magically get connection pooling with any JPA provider. If you import a logging library, it magically works. Flyway and Liquibase db migration tools have built-in failsafes, and "just work" with defaults.
I think Ruby suffers immeasurably from most things being built for Rails. In Java and Python, there's so many options that everything is forced to be modular when running standalone and play nice with various frameworks.
Ruby is loved for Rails, but Rails is slowly killing the langauge by making it hard to do anything without it. For most purposes, Ruby is Rails. To me this is a good reason to use a different language where I have flexible options
There’s a _lot_ of reasons to use Ruby if Rails is off the table.
Unless they changed something in recent years, but to me it's been the worst logging framework I've ever seen.
Oh man, I've been working as a freelance/agency with a a team of 3 experts in nodejs for 5 years now - we've built API & products for many many clients
Our nodejs/express "lightweight" framework is looking exactly like a rails app now (migrations, controllers and models generator, some scaffolding generators)
Honestly, even if doing JSON API -> React frontend, I'd still do it. It's too easy to get it right with Rails.
I've used RoR for 5 years, spring boot/kotlin for 2 years and Django for a year. Once you've set up the workflow, there isn't much difference in terms of productivity. The pain point is setting up the base project and keeping it updated as library versions change.
What django and spring boot are missing are the commandline generators for entities, controllers and views and a quick setup.
Django and spring boot need a little bit of careful setup. Django needs its settings, applications array, middleware declarations, etc. Once you get over this and start adding your entities, views and routes it goes fine.
Spring boot requires a rigid folder structure, application property files, a careful setup for migrations and logging. Liquibase setup, etc. It doesn't get off the ground easily. Once you start writing your entities, repositories, controllers, services, views it goes smoothly thanks to lazy initializers.
It's not a perfect replacement, but it can save time if you are starting a new project with a lot of boilerplate code.
I personally choose Java/SpringBoot, it has a steep learning curve, but has strong typing, runs as fast as most C++ code out there, plus you can do async.
and if you want something fast but that is as easy as Rails and runs on Java/Groovy/JVM, use Grails.
Is there a reason you'd use Grails over Rails on jRuby?
Note: I see that torquebox was discontinued. It was a simple way of getting your rails project to work with JBoss and had many nice utilities like infinispan and stomp. Good old days.
That doesn't seem easy at all. Immediately I am presented with 5 choices and then a list of 50 things that I might want to add to my application. Some of those 50 choices seem to be mutually exclusive and I don't have any way of knowing which I might want or why.
Rails? Just rails new myapp.
Build system, Language, Spring version, Java packaging, Java version
The rest is naming.
That is not complex to me.
As for the dependency options... well, they are dependency options. I'm sure you have no problem with adding gems to a rails project, even though there are thousands of them that may be mutually exclusive! A non-ruby dev might not know which gem they want and would have no way of knowing what they want or why!
rails new myapp is exactly the same as doing this with no dependencies.
From the article: "Rails has less to offer an API server that speaks JSON over the wire."
Everything we do works this way now (JSON over the wire). Everything is a RESTful API first. We build a UI that uses the API, but the business function is served completely from the API. Why? Because if the function is valuable, then some other program will likely want to exercise it programmatically. All of our capabilities are built to be composed programmatically by default.
The additional effects this has on our organization is that we can often turn around new integrations in hours. They're all API calls. Other developers can kick the tires through fully secured Swagger UIs before a pretty application ties it together. And everything we build can be combined into multiple different UIs anywhere in our suite of applications.
Every API is built with the assumption that it is out on the public internet, subject to all the security and scaling problems that entails. What it has driven us to do is get very good at standing up hardened, simple services that host a consistent API-style.
We build UIs, but we assume the real value is in the functionality and the data, not in one specific form of its presentation.
One is serving server side rendered pages. One is providing non-functional requirements such as security (including browser based security, some of which is tied to the pages served, i.e., CSRF tokens). One is providing CRUD functionalities.
The first one, and some of the conveniences of the second, you'd not benefit from. The last one, and some of the core functionalities of the second, you would. So, less to offer, but not 'nothing'.
Never mind a team of 200, even on a team of one person I don't trust myself! That's why I'm using typed FP. I don't trust myself not to make clumsy errors. I don't trust myself to be disciplined and write all the tests. I don't trust myself to not design a naïve architecture which I'll have to do a huge — and dangerous — refactoring on later.
This is why I'm not using Ruby/Rails any longer.
All of my income is from my own software businesses. I used to try to do this with Ruby, and there were lots of problems. Now I do it with Haskell, and there are fewer problems. This ended up being a very good business decision for me.
What is the return on investment for me as a businessman on bothering to "do anything properly" when the resulting product is both cheaper to write and is more stable if I just use a better tool? What do I actually gain? Some kind of nerd-honour?
This point reminds me of some essays from Steve Yegge where he argued that one way of categorising programmers was into two groups: 'conservatives' who liked languages like Haskell where you could have confidence the code worked if it compiled, etc. and 'liberals' who liked languages like Ruby, where if you notice something breaks you just fix it; just write some feature to see if it works or not. -- I'd think for prototyping it's nicer to have the latter attitude, and it's more reasonable you'll have an understanding of the system since it's so fresh. For long term maintenance, the former is surely better
I can't speak for everyone, but my perspective comes from a place of practicality as opposed to philosophy. I (and also my team) try to push the domain we're modelling into the types, but we also write a whole bunch of tests (which includes isolated, integrated, and property-based tests). I've never understood the argument of one vs the other — it's great to have both. Programming is tough so I'll take all the help I can get.
I submit that this sentence:
> Ruby allows for a lot of non-local effects. Whether that’s monkeypatching, writing to a database or creating new types at runtime, Ruby isn’t designed for a team of 200 programmers
can and should end there, full stop.
The bigger your team, no matter how skilled, the more often someone somewhere will make a mistake and cause side effects they didn't intend. And when these side effects can manifest in unfathomable and hard-to-analyze errors in a completely different part of the codebase, you're in trouble.
Obviously, this can happen in any language, but some enable it a lot more than others, and some make analyzing weird behaviour a lot easier than others.
Typed FP for Webdev is surprisingly enjoyable. Working with Frameworks/Stacks that translate FP code to HTML/JS is an absolute joy.
At work we use Yesod, and it’s been working great for us. A more detailed explanation of what we use can be found here[0].
I could write a web server in OCaml or Rust and it'd be wonderful code that has a lower rate of failure per line. But it may not have the same depth of logging, connection pooling, configuration, etc. I may have to write some of my own auth logic, as I had to do when dealing with JWTs in Rust. Do I trust myself to write auth logic? Even with types it's quite easy to write a logic error that leads to a vulnerability. Meanwhile I'm pretty certain Devise is rock solid.
Also things just take longer in less mature stacks. I had a wonderful time writing a web server in Rust but I'll probably never do it again unless I need serious performance and Rust's libraries mature a little. And Rust's library ecosystem is a lot better than say, OCaml's or Haskell's. Robust code is great, but it's meaningless if you can't get stuff done.
If your business model brings in $X per unit of usage your hosting costs need to be < $X per unit of usage or else no amount of engineering will make your business profitable.
One of the founders of Twitch recently tweeted about how using Amazon's new service for Twitch-like video streaming simply wouldn't be feasible for building your own Twitch.
But there are very few cases. Fewer I'd say than this article lists. Low level programming, and yes perhaps event-based systems. [1]
I find people just like rewriting their own batteries and conventions. Fun, no doubt, and a good learning experience.
I challenge any microframework / library user to surpass the productivity of a mature framework like Rails or Django. I argue nothing comes close.
And I don't mean "for the 5 second MVP". Perhaps your app won't need some of the batteries, but your app will need logging. It will need database migrations. It will need forms and inputs. It will need media uploads. It will need to send email. And so on.
Future proofing too much is an issue in the industry. YAGNI after all. But this is the opposite case. I deem it: YAEGNI. You are eventually going to need it.
[1] Django 3.1, just released yesterday, begins treating async as a first class citizen. I was initially opposed to this, given 99% of the time you don't need async. But I suppose it's nice to have and drop down to.
Yea it's easy. But why are you writing a server? That should be the least of your concerns.
The classic response is “zomg premature optimization bad”, but some of us work on more complex applications than simple database CRUD.
Ruby/Rails and Elixir/Phoenix has one big thing, REPL. No matter what you choose. C++, Go, Rust, Node etc... getting solid easy to use REPL is effort and not built in feature.
in Rails/Phoenix you get it for free. That is a great perk!
Maybe I'm just too used to Lisps (and Ruby, which also handled it better)
What do you mean by this ? It's been a while since I was able to use await in REPL, maybe you are referring to something else ?
Once they added await to REPL I didn't really have issues with testing async code, but again I only did small projects with node.
In my opinion, Phoenix competes well with most of the strong points of Rails, and avoids many of the downsides - including the ones discussed in this article. It's also built on a more performant and robust foundation (Erlang/OTP). You may not truly "need" some of those advanced foundation features at first, but at just the same point where a Rails project starts to struggle, Phoenix really shines.
Phoenix is not the solution for every app, but for every app where I would have used Rails in the past, it's looking like Phoenix from now on.
It's somewhat understandable that the official websites of frameworks/tools/libraries/languages/services tend towards espousing the advantages of the thing and leaving out most of the drawbacks. That's just how the incentives are aligned. But even the average blog article about a tool posted on HN rarely talks about what situations it is most effective, and what situations it should potentially be avoided. I suppose it's a natural consequence of blog authors only knowing so many tools, no single person knows them all.
I'm not saying this information isn't out there – just that it seems like the exception rather than the rule.
One of the goals of a podcast I started last year was to answer that type of question and more.
It's mainly a podcast where I talk to someone new every week to get a better understanding of why they chose a specific tech stack to build their app but it also goes into the "how" since we talk about how they build and deploy their app, lessons learned along the way, etc..
A list of episodes can be found here: https://runninginproduction.com/podcast/
Each episode has a "Motivation for using ..." time stamp so you can jump to that part of the discussion. It's usually within the first 5 or 10 minutes.
The goal here is to have a bunch of real world examples of why and when someone used X and they usually spend a few minutes talking about why they chose that instead of something else too. There's episodes ranging from solo developers working on a side project to high traffic services handling billions of requests per month.
The idea is to have loads of practical examples instead of trying to theory craft hypothetical scenarios.
Around 4-5 months ago I implemented a custom player so now it's really easy to listen at 2x speed, and there's always been timestamped links to quickly jump around to specific topics.
Edit; it was a question, what's with the downvotes? It used to be bad; I'm not the only one saying that. It's normal (Laravel, Node etc have it as well; it's fast moving).
- Bundler makes keeping track of gems trivial
- Rails defaults are explicitly version-bound. E.g. you can update to Rails 6 and keep using Rails 5 defaults.
- There are now extensive official guides on how to update.
When It’s ‘Just’ an API Server: Rails has less to offer an API server that speaks JSON over the wire. A lot of its HTTP security doesn’t matter for that case (e.g. SQL injection safeguards, XSS prevention.)
It seems to me that SQL injection safeguards would still be needed in an API service. OFC XSS isn't relevant though.
(I word it like this to be charitable in case I misunderstand what the author was trying to say, but if they are trying to say what I think they are, I think they are wrong.)
Obviously if you write sufficiently bad ActiveRecord method calls, you can trick it into doing SQL Injection. So it's not magic. (Indeed, I view that as a strength of AR; it has robust escape hatches. I hate ORMs, but I hate AR less than any other ORM.)
And equally obviously, every credible database driver written in at least the last 15 years has robust support for prepared statements and tolerable documentation on how to use it that can be understood in literally a few minutes. Which, of course, is exactly how AR gets it right in the first place.
(And I only have to say "in the last 15 years" because of the dumpster fire that was turn-of-the-century MySQL.)
I played with F# and liked it enough but didn't use much of a framework (Giraffe). Biggest hurdle was there wasn't a good up-to-date setup guide I could find for macOS.
That API part in particular is dead wrong. Gives the feeling Rails is not good for anything.
The one I'm currently working on is blazing fast and were truly hitting postgres limits... At nearly 50k requests a second.
The DB is rarely a bottleneck if you have sane structure and indexes. Ironically the slowest frameworks also tend to generate terrible queries (ActiveRecord, SQLAlchemy), so you end up with a slow database and web framework
When our team determined React to be the way to go for the front end, I looked at all of the parallel dependency management and the backend complexity (one might call “sophistication”) of Rails, and ultimately decided to make the backend in Node instead.
Has anyone here experienced pleasant success with RoR+SPA?
If you already can write a Hello World back-end in some other language/framework, and you're now learning Rails just so you can do Rails+React, that's a dumb waste of time.
(I'd say the same thing about most frameworks for most purposes, but it's especially true with Rails. But doing Rails+React when you're already fast and competent with Rails, that's a very sensible choice in most situations.)
Before the API thing, I also developed websites with fullstack Rails. The experience with fullstack Rails and Rails API have helped me a lot to understand the requirement of a backend framework. I'm forever endebted to Rails for this.
I disagree on the SQL injection part, Api calls can contain user input which in turn can be used to perform SQL injection, I am not sure what the difference between a website and an api server is in regard to this concern.