100 days of Ruby on Rails: the recap — a practical guide
We've spent 100 days building with Ruby on Rails, and it's time to distill what actually matters. The framework has matured dramatically, and the lessons that separate working applications from production-grade systems are clearer than ever. This recap covers the patterns, practices, and decisions that shape real-world Rails projects.
Conventions are speed — Follow Rails defaults until you have a reason not to
Rails was built on a core principle: convention over configuration. After 100 days, this isn't just philosophy—it's measurable productivity. When you follow the framework's defaults for naming, file structure, and patterns, you reduce cognitive load across your team and ship features faster.
The moment we deviate from conventions, we pay a tax. A controller named UserManager instead of UsersController, models in a non-standard directory, or routes that don't follow REST patterns force every developer to learn your custom rules before they can be effective. In a production environment, that cost multiplies across hiring cycles and onboarding.
Consider a standard Rails resource:
class UsersController < ApplicationController
def index
@users = User.all
end
def show
@user = User.find(params[:id])
end
def new
@user = User.new
end
def create
@user = User.new(user_params)
if @user.save
redirect_to @user, notice: 'User created'
else
render :new
end
end
private
def user_params
params.require(:user).permit(:name, :email)
end
endThis structure is instantly recognizable to any Rails developer. The flow, variable names, and action sequence are predictable. Your team doesn't need documentation to understand it. When you're hiring mid-level engineers or onboarding contractors, they can contribute within hours rather than days.
Rails conventions also unlock framework magic. Nested routes, automatic scoping, association inference—all of these work because you follow the patterns. Deviate, and you lose the leverage.
The rule we've seen work best: follow conventions by default. When you need to break them—perhaps for a complex domain model or unusual deployment constraint—make that decision explicitly as a team, document it, and apply it consistently.
Active Record needs care — Includes, indexes and transactions
Active Record is Rails' superpower and its common pitfall. The abstraction is so smooth that teams often ship N+1 queries, missing indexes, and race conditions into production. By day 100, the painful lesson is clear: Active Record requires intentional performance thinking.
The N+1 problem is the classic trap. You fetch a list of users, then in your view, you reference user.posts.count for each user. Active Record fires one query per user. With 100 users, that's 101 queries instead of 2.
# Bad: N+1 queries
@users = User.all
@users.each do |user|
puts user.posts.count # Fires a query for each user
end
# Good: eager load
@users = User.includes(:posts)
@users.each do |user|
puts user.posts.count # Uses pre-loaded data
end
# Or, if you just need the count
@users = User.select('users.*, COUNT(posts.id) as posts_count')
.joins(:posts)
.group('users.id')In a real e-commerce application, this difference between includes and lazy loading could turn a 50ms endpoint into a 2-second endpoint once you reach scale. The user experience degrades silently until monitoring catches it.
Indexes are equally critical. Every query against a large table should hit an indexed column. Without an index, PostgreSQL scans the entire table. With 10 million records, that's not a performance issue—it's a business-stopping incident.
# Migration: add index for common queries
class AddIndexToUsersEmail < ActiveRecord::Migration[7.0]
def change
add_index :users, :email, unique: true
add_index :users, :created_at
add_index :orders, [:user_id, :status] # Composite index for filtering
end
endTransactions protect data integrity during concurrent operations. In a payment system, a race condition where two requests process simultaneously can result in duplicate charges or lost refunds.
# Without transaction: race condition
user.update(balance: user.balance - amount)
charge_card(card, amount)
# With transaction: atomic operation
User.transaction do
user.lock! # Lock the row
user.update(balance: user.balance - amount)
charge_card(card, amount)
endThe lock ensures that between reading the balance and updating it, no other request can modify the same user. This pattern is essential for any operation where state matters.
Hotwire simplifies UI — Server-rendered apps that feel modern
For years, Rails teams built separate JavaScript SPAs or shipped jQuery soup. Hotwire changed the conversation. With Turbo and Stimulus, you build interactive, responsive interfaces without leaving Rails or writing heavy JavaScript.
Hotwire is built on server-side rendering with smart partial updates. When a user submits a form, the server renders only the changed content and broadcasts it to the client. The page feels instant because there's no serialization delay or client-side state management nightmare.
A concrete example: a user creates a todo in a shared list. Without Hotwire, you'd build a React component that posts JSON, waits for the response, and updates state. With Hotwire, you render a partial and let Turbo push it to all connected clients:
# Controller action
class TodosController < ApplicationController
def create
@todo = Todo.new(todo_params)
if @todo.save
broadcast_append_to "todos", target: "todos-list", partial: "todos/todo", locals: { todo: @todo }
redirect_to todos_path, notice: 'Todo created'
else
render :new
end
end
end
# View partial (_todo.html.erb)
<li>
<p><%= todo.title %></p>
<%= link_to "Delete", todo, method: :delete, remote: true %>
</li>Hotwire broadcasts this partial to all clients watching the "todos" channel. Users see the new todo appear instantly without refresh. The entire interaction is server-driven, so your team owns the logic end-to-end.
Stimulus handles interactivity that Turbo doesn't cover. Toggle buttons, dropdowns, form validation—these are lightweight JavaScript controllers that respond to DOM events. The framework keeps you in Rails territory; you're writing small, focused controllers rather than managing a full client-side app.
By day 100, teams using Hotwire ship features faster than those managing separate frontend frameworks. You reduce context-switching, eliminate API design friction, and keep your entire codebase in Ruby.
Rails 8 simplifies ops — Solid adapters and Kamal deployments
Rails 8 brought significant improvements to deployment and infrastructure. The SOLID Queue, Solid Cable, and Kamal integration mean you can run a complete Rails application on a single server or small cluster without Redis, Sidekiq, ActionCable infrastructure, or complex DevOps tooling.
For teams running trading platforms, ride-hailing, or other latency-sensitive systems, this matters. Previously, you needed Redis for caching, Sidekiq for background jobs, and a separate WebSocket server for real-time features. Now, Rails handles all three natively.
# config/solid_queue.yml
production:
dispatchers:
- batch_size: 500
polling_interval: 1
workers:
- queues: [default, critical]
threads: 5
processes: 2Solid Queue is a database-backed job queue. Jobs live in your PostgreSQL database, workers poll for new jobs, and there's no separate Redis process to manage. For small to medium deployments, this eliminates infrastructure complexity.
Kamal is the deployment tool that changed shipping Rails. It uses Docker and SSH to deploy applications anywhere—your own hardware, cloud VMs, or bare metal. You write a simple configuration file and run kamal deploy. Kamal handles rolling deployments, health checks, and secrets management.
# config/deploy.yml
service: myapp
image: myapp:latest
servers:
web:
hosts:
- 192.168.1.100
options:
memory: "1GB"
registry:
username: deploy
password:
- ENV['REGISTRY_PASSWORD']
env:
secret:
- RAILS_MASTER_KEY
- DATABASE_URLKamal removes the DevOps tax for small teams. You don't need a Kubernetes cluster or complex CI/CD. You push code, the Docker image builds, and it deploys to your servers. Rails 8 made this workflow first-class.
For a SaaS platform or internal tool, this changes the economics. One or two engineers can now operate a production Rails application without hiring dedicated platform engineers.
Common mistakes
- Ignoring N+1 queries until monitoring alerts fire. By then, you've already shipped to production. Use Rails Query Log Tagging and monitor query counts in development.
- Skipping database migrations for schema changes. Raw SQL or manual schema modifications break consistency and make rollbacks impossible. Always use migrations.
- Over-engineering abstractions early. A custom job queue, a service object layer, a custom authentication system—these feel smart until they break and no one understands them. Follow Rails conventions until you have concrete evidence you need something different.
- Treating Hotwire as incomplete. Some teams assume they still need React for "real" interactivity. Hotwire + Stimulus handles 95% of modern UI requirements. Only reach for a separate framework if you have genuine constraints.
- Deploying manually or with ad-hoc scripts. Kamal standardizes deployment. Use it. Your future self will thank you when you onboard the next engineer.
Key takeaways
- Rails conventions exist for a reason. Following them reduces context-switching and ship time across the entire team.
- Active Record is powerful but requires discipline: use
includesfor associations, add indexes for query columns, and wrap state-changing operations in transactions. - Hotwire brings modern interactivity to server-rendered Rails applications without the complexity of separate frontend frameworks.
- Rails 8 simplifies operations. Solid Queue, Solid Cable, and Kamal let small teams run production systems without significant infrastructure overhead.
- The best Rails decision is often the simplest one. Ship the conventional approach first, monitor in production, and optimize when data tells you there's a real problem.






Comments
Loading comments…