Ruby on Rails: Why It's Still Shipping Products Faster Than Anything Else
A practical guide to Ruby on Rails — convention over configuration, Active Record, Hotwire, and why startups still choose Rails to ship fast.
Every few years, someone declares Ruby on Rails dead. They point to newer frameworks, faster languages, trendier ecosystems. And every few years, another batch of startups launches on Rails and ships their product in weeks instead of months.
Shopify runs on Rails. GitHub ran on Rails for over a decade. Basecamp, Airbnb, Twitch, Hulu, Kickstarter — all built on Rails. In 2026, Hey.com, Linear's backend, and countless Y Combinator startups are still choosing Rails for new projects. Not because they haven't heard of Go or Next.js, but because when the goal is to ship a working product and iterate quickly, Rails remains absurdly productive.
The question isn't whether Rails is "still relevant." It's why it keeps being the right choice for a specific and very common use case: building web applications where developer productivity matters more than raw performance.
Convention Over Configuration
This is the core philosophy, and understanding it is understanding Rails. In most frameworks, you make decisions: Where do files go? How do you name database tables? What ORM do you use? How do you structure routes? Rails answers all of these questions for you.
Database table for a User model? It's called users. Controller for users? UsersController in app/controllers/users_controller.rb. Views for users? app/views/users/. Primary key? id. Timestamps? created_at and updated_at. Foreign key from posts to users? user_id.
These conventions aren't suggestions — they're the defaults that the entire framework relies on. When you follow them, everything works together automatically. When you need to break them, you can, but you do it explicitly and deliberately.
This sounds restrictive until you realize how much decision fatigue it eliminates. A Rails developer can join any Rails project and immediately know where everything is. The mental overhead of "how does this project structure things" drops to near zero.
Getting Started in Five Minutes
# Install Rails (assuming Ruby is installed)
gem install rails
# Create a new application
rails new myapp --database=postgresql
# Enter the project
cd myapp
# Create the database
rails db:create
# Start the server
rails server
You now have a running web application at localhost:3000 with a PostgreSQL database, an asset pipeline, a test framework, and a development environment with automatic code reloading.
The MVC Structure
Rails follows the Model-View-Controller pattern rigidly. Every piece of code has a clear home:
# app/models/article.rb — the model
class Article < ApplicationRecord
belongs_to :author, class_name: 'User'
has_many :comments, dependent: :destroy
has_many :taggings
has_many :tags, through: :taggings
validates :title, presence: true, length: { minimum: 5, maximum: 200 }
validates :body, presence: true
validates :status, inclusion: { in: %w[draft published archived] }
scope :published, -> { where(status: 'published') }
scope :recent, -> { order(published_at: :desc) }
scope :by_tag, ->(tag) { joins(:tags).where(tags: { name: tag }) }
before_publish :set_published_at
def reading_time
(body.split.size / 200.0).ceil
end
private
def set_published_at
self.published_at ||= Time.current
end
end
# app/controllers/articles_controller.rb — the controller
class ArticlesController < ApplicationController
before_action :authenticate_user!, except: [:index, :show]
before_action :set_article, only: [:show, :edit, :update, :destroy]
before_action :authorize_author!, only: [:edit, :update, :destroy]
def index
@articles = Article.published.recent
@articles = @articles.by_tag(params[:tag]) if params[:tag].present?
@articles = @articles.page(params[:page]).per(20)
end
def show
@comments = @article.comments.includes(:user).order(created_at: :asc)
@new_comment = Comment.new
end
def new
@article = Article.new
end
def create
@article = current_user.articles.build(article_params)
if @article.save
redirect_to @article, notice: 'Article was successfully created.'
else
render :new, status: :unprocessable_entity
end
end
def update
if @article.update(article_params)
redirect_to @article, notice: 'Article was successfully updated.'
else
render :edit, status: :unprocessable_entity
end
end
def destroy
@article.destroy
redirect_to articles_path, notice: 'Article was deleted.'
end
private
def set_article
@article = Article.find(params[:id])
end
def authorize_author!
redirect_to articles_path unless @article.author == current_user
end
def article_params
params.require(:article).permit(:title, :body, :status, tag_ids: [])
end
end
Strong parameters (article_params) prevent mass assignment vulnerabilities — only explicitly permitted fields can be set from form data. This is security by default, not security by remembering to add it.
Active Record — The ORM That Defined ORMs
Active Record is the pattern where each database table maps to a Ruby class, each row maps to an object, and each column maps to an attribute. Rails' implementation of this pattern is arguably the most influential ORM ever built:
# Migrations define database schema changes
class CreateArticles < ActiveRecord::Migration[7.2]
def change
create_table :articles do |t|
t.string :title, null: false
t.text :body, null: false
t.string :status, default: 'draft'
t.references :author, null: false, foreign_key: { to_table: :users }
t.datetime :published_at
t.timestamps
end
add_index :articles, :status
add_index :articles, :published_at
end
end
# Active Record queries — Ruby, not SQL
# Find published articles from the last week
Article.published
.where(published_at: 1.week.ago..)
.order(published_at: :desc)
.limit(10)
# Complex queries with joins
User.joins(:articles)
.where(articles: { status: 'published' })
.group(:id)
.having('COUNT(articles.id) > 5')
.select('users.*, COUNT(articles.id) as article_count')
# Eager loading to prevent N+1 queries
Article.published
.includes(:author, :tags, comments: :user)
.recent
.page(1).per(20)
# Transactions
ActiveRecord::Base.transaction do
order = Order.create!(user: current_user, total: cart.total)
cart.items.each do |item|
order.line_items.create!(product: item.product, quantity: item.quantity)
item.product.decrement!(:stock, item.quantity)
end
cart.destroy!
end
The .includes call is critical. Without it, displaying 20 articles with their authors, tags, and comments would fire potentially hundreds of database queries (the N+1 problem). With it, Rails executes 4-5 queries regardless of how many articles there are.
Generators — Scaffolding That Actually Helps
Rails generators create files, write boilerplate, and set up database migrations in one command:
# Generate a model with migration
rails generate model Comment body:text user:references article:references
# Generate a controller with actions
rails generate controller Dashboard index stats
# Generate a complete scaffold (model, controller, views, tests, migration)
rails generate scaffold Product name:string description:text price:decimal{10,2} stock:integer
# Run migrations
rails db:migrate
The scaffold generator is controversial. Some developers think it generates too much code. Others use it as a starting point and customize from there. Either way, it demonstrates Rails' philosophy: get something working immediately, then refine.
Routing
Rails routes map URLs to controller actions with a clean DSL:
# config/routes.rb
Rails.application.routes.draw do
root 'pages#home'
# RESTful routes — generates 7 standard routes
resources :articles do
resources :comments, only: [:create, :destroy]
member do
patch :publish
patch :archive
end
collection do
get :drafts
end
end
resources :users, only: [:show, :edit, :update]
# Authentication
get 'login', to: 'sessions#new'
post 'login', to: 'sessions#create'
delete 'logout', to: 'sessions#destroy'
# API namespace
namespace :api do
namespace :v1 do
resources :articles, only: [:index, :show]
resources :users, only: [:show]
end
end
end
resources :articles generates seven routes: index, show, new, create, edit, update, destroy. Each maps to a controller action with the expected HTTP method. This RESTful routing convention means URLs are predictable across every Rails application.
Views and Hotwire
Rails traditionally rendered HTML on the server. In an era where single-page applications dominate, Rails doubled down on this approach with Hotwire — a set of tools that give server-rendered HTML the interactivity of an SPA.
<%# app/views/articles/index.html.erb %>
<h1>Articles</h1>
<div class="filters">
<%= form_with url: articles_path, method: :get, data: { turbo_frame: "articles" } do |f| %>
<%= f.text_field :search, value: params[:search], placeholder: "Search..." %>
<%= f.select :tag, Tag.pluck(:name), { include_blank: "All tags" }, {} %>
<%= f.submit "Filter" %>
<% end %>
</div>
<%= turbo_frame_tag "articles" do %>
<div class="article-grid">
<% @articles.each do |article| %>
<%= render article %>
<% end %>
</div>
<div class="pagination">
<%= paginate @articles %>
</div>
<% end %>
<%# app/views/articles/_article.html.erb — a partial %>
<article class="article-card" id="<%= dom_id(article) %>">
<h2><%= link_to article.title, article %></h2>
<div class="meta">
<span>By <%= article.author.name %></span>
<span><%= time_ago_in_words(article.published_at) %> ago</span>
<span><%= article.reading_time %> min read</span>
</div>
<p><%= truncate(article.body, length: 200) %></p>
<div class="tags">
<% article.tags.each do |tag| %>
<%= link_to tag.name, articles_path(tag: tag.name), class: "tag" %>
<% end %>
</div>
</article>
Turbo (part of Hotwire) intercepts link clicks and form submissions, makes them via fetch, and swaps HTML on the page without a full reload. Turbo Frames scope updates to parts of the page. Turbo Streams push real-time updates from the server. The result feels like an SPA but you're writing server-rendered HTML with zero JavaScript.
For the interactions that do need JavaScript, Stimulus provides a lightweight controller framework:
// app/javascript/controllers/dropdown_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["menu"]
static classes = ["open"]
toggle() {
this.menuTarget.classList.toggle(this.openClass)
}
close(event) {
if (!this.element.contains(event.target)) {
this.menuTarget.classList.remove(this.openClass)
}
}
}
<div data-controller="dropdown" data-action="click@window->dropdown#close">
<button data-action="dropdown#toggle">Menu</button>
<ul data-dropdown-target="menu" data-dropdown-open-class="visible">
<li><a href="/profile">Profile</a></li>
<li><a href="/settings">Settings</a></li>
</ul>
</div>
No build step required for basic Stimulus controllers. No virtual DOM, no state management library, no component lifecycle to manage. You write HTML, sprinkle behavior on it with data attributes, and it works.
The Rails Philosophy
Rails has opinions that go beyond code structure:
Programmer happiness matters. Ruby was designed to make programmers happy, and Rails extends that philosophy. Code should be enjoyable to write and read. This isn't fluffy — happy developers are productive developers, and code that reads clearly is code that has fewer bugs. Optimize for the common case. Most web applications need CRUD operations, authentication, background jobs, email sending, and file uploads. Rails has built-in solutions for all of these. If you're building something unusual, Rails might feel constraining. If you're building what most people build, Rails is remarkably fast. Majestic monolith. Rails creator DHH actively advocates for monolithic applications over microservices for most teams. One codebase, one deploy, one thing to monitor. This is controversial in 2026, but companies like Shopify and Basecamp prove it works at scale.When Rails Is the Right Choice
Choose Rails when:
- You need to ship a working product in weeks, not months
- Your application is "standard" web — CRUD, users, content, e-commerce, dashboards
- Your team is small (1-10 developers) and needs to move fast
- You value developer happiness and beautiful code
- You want a mature, stable framework with 20 years of battle-testing
- You need extreme performance (Rails is fast enough for most applications, but not for high-frequency trading)
- Your application is primarily real-time (though Action Cable and Hotwire handle many real-time use cases)
- Your team already knows Python, Java, or Go and switching languages adds risk
- You're building a single-page application with a separate API — Rails works for this, but a lighter backend might be simpler
Why Rails Is Still Here
Rails survives because it solves a real, persistent problem: most software projects fail not because of technical limitations but because they run out of time, money, or motivation before shipping. Rails dramatically shortens the path from idea to working product.
The framework has also continued evolving. Rails 7+ with Hotwire brought modern interactivity without JavaScript framework complexity. Rails 7.1 added built-in authentication generators. Rails 8 brought Kamal for deployment and Solid Queue for background jobs. The ecosystem isn't stagnating — it's doubling down on making the "one person framework" vision real.
If you're interested in learning Ruby and building web applications with elegant code, start with the official Rails guides at guides.rubyonrails.org. For building the programming fundamentals that make framework learning easier, try the challenges on CodeUp to strengthen your problem-solving skills before diving into any framework.