March 26, 202610 min read

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.

ruby rails web-development backend framework
Ad 336x280

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
Think twice when:
  • 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.

Ad 728x90