Preguntas de entrevista Ruby on Rails: Top 25 en 2026
Las 25 preguntas de entrevista Ruby on Rails más solicitadas. Arquitectura MVC, Active Record, migraciones, testing RSpec, APIs REST con respuestas detalladas y ejemplos de código.

Las entrevistas de Ruby on Rails evalúan el dominio del framework Ruby más popular, la comprensión de la arquitectura MVC, el ORM Active Record y la capacidad de construir aplicaciones web robustas siguiendo la filosofía "Convention over Configuration". Esta guía cubre las 25 preguntas más solicitadas, desde los fundamentos de Rails hasta los patrones avanzados de producción.
Los reclutadores valoran a los candidatos que comprenden la filosofía Rails: "Convention over Configuration", DRY (Don't Repeat Yourself) y los patrones Rails Way. Explicar por qué Rails toma ciertas decisiones arquitectónicas marca la diferencia.
Fundamentos de Ruby on Rails
Pregunta 1: Explicar el patrón MVC en Ruby on Rails
El patrón Model-View-Controller (MVC) es el núcleo arquitectónico de Rails. Separa las responsabilidades en tres capas distintas para una mejor mantenibilidad y testabilidad del código.
# app/models/article.rb
# El Model gestiona los datos y la lógica de negocio
class Article < ApplicationRecord
# Validaciones de datos
validates :title, presence: true, length: { minimum: 5 }
validates :body, presence: true
# Asociaciones con otros modelos
belongs_to :author, class_name: 'User'
has_many :comments, dependent: :destroy
has_many :tags, through: :article_tags
# Scopes para consultas reutilizables
scope :published, -> { where(published: true) }
scope :recent, -> { order(created_at: :desc).limit(10) }
# Callbacks del ciclo de vida
before_save :generate_slug
private
def generate_slug
self.slug = title.parameterize if title_changed?
end
end# app/controllers/articles_controller.rb
# El Controller recibe las peticiones y orquesta la respuesta
class ArticlesController < ApplicationController
before_action :authenticate_user!, except: [:index, :show]
before_action :set_article, only: [:show, :edit, :update, :destroy]
def index
@articles = Article.published.recent.includes(:author)
end
def show
@comments = @article.comments.includes(:user)
end
def create
@article = current_user.articles.build(article_params)
if @article.save
redirect_to @article, notice: 'Artículo creado correctamente.'
else
render :new, status: :unprocessable_entity
end
end
private
def set_article
@article = Article.find(params[:id])
end
def article_params
params.require(:article).permit(:title, :body, :published, tag_ids: [])
end
end<%# app/views/articles/show.html.erb %>
<%# La View muestra los datos en formato HTML %>
<article class="article-detail">
<header>
<h1><%= @article.title %></h1>
<p class="meta">
Por <%= @article.author.name %> •
<%= l @article.created_at, format: :long %>
</p>
</header>
<div class="content">
<%= simple_format @article.body %>
</div>
<%# Partial para los comentarios %>
<%= render @comments %>
</article>El flujo típico: la petición llega al Router, que la despacha al Controller adecuado. El Controller interactúa con el Model para recuperar o modificar los datos, luego pasa esos datos a la View para el renderizado HTML.
Pregunta 2: ¿Qué es Active Record y cómo funciona el ORM de Rails?
Active Record es el ORM (Object-Relational Mapping) de Rails que implementa el patrón Active Record. Cada clase Model representa una tabla de la base de datos, y cada instancia representa una fila.
# app/models/user.rb
# Active Record mapea automáticamente columnas a atributos
class User < ApplicationRecord
# La tabla 'users' se asocia automáticamente
# Columnas: id, email, name, created_at, updated_at
has_secure_password # BCrypt para la contraseña
has_many :articles, foreign_key: :author_id
has_one :profile, dependent: :destroy
has_and_belongs_to_many :roles
# Validaciones
validates :email, presence: true,
uniqueness: { case_sensitive: false },
format: { with: URI::MailTo::EMAIL_REGEXP }
# Callbacks
before_save :normalize_email
# Métodos de clase para consultas
def self.admins
joins(:roles).where(roles: { name: 'admin' })
end
private
def normalize_email
self.email = email.downcase.strip
end
end# Ejemplos de consultas Active Record
# Consola Rails o dentro de un servicio
# Creación
user = User.create!(email: 'dev@example.com', name: 'Alice', password: 'secret123')
# Lectura con condiciones
active_users = User.where(active: true).order(:name)
user = User.find_by(email: 'dev@example.com')
# Consultas encadenadas (evaluación perezosa)
recent_admins = User.admins
.where('created_at > ?', 1.month.ago)
.includes(:profile)
.limit(10)
# Prevención de N+1 con eager loading
articles = Article.includes(:author, :comments).published
# Actualización
user.update!(name: 'Alice Martin')
# Transacciones
User.transaction do
user.debit_balance!(100)
recipient.credit_balance!(100)
Payment.create!(from: user, to: recipient, amount: 100)
endActive Record convierte los métodos Ruby en consultas SQL optimizadas. Los métodos como where, joins, includes son perezosos: la consulta solo se ejecuta al iterar o al llamar to_a.
Pregunta 3: Explicar el sistema de migraciones de Rails
Las migraciones permiten versionar el esquema de la base de datos con Ruby. Son reversibles y permiten una evolución controlada de la estructura de datos.
# db/migrate/20260203100000_create_products.rb
# Migración para crear una tabla
class CreateProducts < ActiveRecord::Migration[7.1]
def change
create_table :products do |t|
t.string :name, null: false
t.text :description
t.decimal :price, precision: 10, scale: 2, null: false
t.integer :stock_quantity, default: 0
t.references :category, null: false, foreign_key: true
t.boolean :active, default: true
t.timestamps # created_at y updated_at automáticos
end
# Índices para el rendimiento
add_index :products, :name
add_index :products, [:category_id, :active]
end
end# db/migrate/20260203110000_add_slug_to_products.rb
# Migración para modificar una tabla existente
class AddSlugToProducts < ActiveRecord::Migration[7.1]
def change
add_column :products, :slug, :string
add_index :products, :slug, unique: true
# Rellenar slugs existentes
reversible do |dir|
dir.up do
Product.find_each do |product|
product.update_column(:slug, product.name.parameterize)
end
end
end
# Hacer no nulo después del relleno
change_column_null :products, :slug, false
end
end# Comandos esenciales de migración
rails db:migrate # Ejecutar migraciones pendientes
rails db:rollback # Deshacer la última migración
rails db:rollback STEP=3 # Deshacer las últimas 3 migraciones
rails db:migrate:status # Ver el estado de las migraciones
rails db:seed # Ejecutar db/seeds.rb
rails db:reset # Drop, create, migrate, seedLas migraciones deben ser reversibles. El método change es inteligente y puede revertir automáticamente las operaciones comunes. Para casos complejos, usar up y down por separado.
Active Record avanzado
Pregunta 4: ¿Cómo optimizar consultas N+1 en Rails?
El problema N+1 ocurre cuando una consulta inicial es seguida por N consultas adicionales para cargar las asociaciones. Rails ofrece varios métodos de eager loading para resolver este problema.
# app/controllers/orders_controller.rb
class OrdersController < ApplicationController
def index
# ❌ PROBLEMA N+1: 1 consulta + N consultas por pedido
# @orders = Order.all
# En la vista: order.user.name genera una consulta por pedido
# ✅ SOLUCIÓN con includes (eager loading)
@orders = Order.includes(:user, :items)
.where(status: 'completed')
.order(created_at: :desc)
# Genera solo 3 consultas en total
end
def show
# includes: carga las asociaciones por separado (2-3 consultas)
@order = Order.includes(items: :product).find(params[:id])
# preload: fuerza la carga separada
@order = Order.preload(:items, :user).find(params[:id])
# eager_load: fuerza un LEFT OUTER JOIN (1 consulta)
@order = Order.eager_load(:items).find(params[:id])
end
end# app/models/order.rb
class Order < ApplicationRecord
belongs_to :user
has_many :items, class_name: 'OrderItem'
has_many :products, through: :items
# Scope con includes por defecto
scope :with_details, -> { includes(:user, items: :product) }
# Counter cache para evitar consultas COUNT
# Requiere: add_column :users, :orders_count, :integer, default: 0
belongs_to :user, counter_cache: true
end# Detección de N+1 con la gem Bullet (desarrollo)
# config/environments/development.rb
config.after_initialize do
Bullet.enable = true
Bullet.alert = true
Bullet.bullet_logger = true
Bullet.rails_logger = true
end
# Bullet mostrará alertas cuando:
# - Se detecte una consulta N+1
# - Haya eager loading innecesario
# - Se debería usar un counter cacheLa regla: usar includes por defecto (Rails elige la estrategia óptima), preload cuando se quiere forzar consultas separadas, eager_load cuando se filtra sobre las asociaciones.
Pregunta 5: Explicar Scopes y Query Objects en Rails
Los scopes encapsulan condiciones de consulta reutilizables. Para consultas complejas, los Query Objects ofrecen mejor organización y testabilidad.
# app/models/product.rb
class Product < ApplicationRecord
# Scopes simples
scope :active, -> { where(active: true) }
scope :in_stock, -> { where('stock_quantity > 0') }
scope :featured, -> { where(featured: true) }
# Scopes con parámetros
scope :cheaper_than, ->(price) { where('price < ?', price) }
scope :in_category, ->(category) { where(category: category) }
# Scopes encadenables
scope :available, -> { active.in_stock }
# Scope con joins
scope :with_recent_orders, -> {
joins(:order_items)
.where('order_items.created_at > ?', 30.days.ago)
.distinct
}
# Scope con subconsulta
scope :bestsellers, -> {
where(id: OrderItem.group(:product_id)
.order('COUNT(*) DESC')
.limit(10)
.select(:product_id))
}
end# app/queries/products_search_query.rb
# Query Object para búsquedas complejas
class ProductsSearchQuery
def initialize(relation = Product.all)
@relation = relation
end
def call(params)
@relation = filter_by_category(params[:category])
@relation = filter_by_price_range(params[:min_price], params[:max_price])
@relation = filter_by_search(params[:q])
@relation = apply_sorting(params[:sort])
@relation
end
private
def filter_by_category(category)
return @relation if category.blank?
@relation.where(category_id: category)
end
def filter_by_price_range(min, max)
@relation = @relation.where('price >= ?', min) if min.present?
@relation = @relation.where('price <= ?', max) if max.present?
@relation
end
def filter_by_search(query)
return @relation if query.blank?
@relation.where('name ILIKE ? OR description ILIKE ?',
"%#{query}%", "%#{query}%")
end
def apply_sorting(sort)
case sort
when 'price_asc' then @relation.order(price: :asc)
when 'price_desc' then @relation.order(price: :desc)
when 'newest' then @relation.order(created_at: :desc)
else @relation.order(:name)
end
end
end
# Uso en el controller
@products = ProductsSearchQuery.new(Product.active).call(params)Los scopes son perfectos para condiciones simples y reutilizables. Los Query Objects son adecuados para búsquedas complejas con múltiples filtros opcionales y lógica de composición.
¿Listo para aprobar tus entrevistas de Ruby on Rails?
Practica con nuestros simuladores interactivos, flashcards y tests técnicos.
Routing y Controllers
Pregunta 6: ¿Cómo funciona el routing RESTful en Rails?
Rails fomenta rutas RESTful que mapean los verbos HTTP a las acciones CRUD. El router traduce las URL en llamadas específicas al controller.
# config/routes.rb
Rails.application.routes.draw do
# Rutas RESTful estándar (7 acciones)
resources :articles do
# Rutas anidadas
resources :comments, only: [:create, :destroy]
# Rutas de miembro (actúan sobre una instancia)
member do
post :publish
delete :archive
end
# Rutas de colección (actúan sobre la colección)
collection do
get :drafts
get :search
end
end
# Rutas de API con namespace
namespace :api do
namespace :v1 do
resources :products, only: [:index, :show, :create, :update] do
resources :reviews, shallow: true
end
end
end
# Ruta personalizada
get 'dashboard', to: 'dashboard#index'
# Restricciones de ruta
constraints(SubdomainConstraint.new) do
resources :admin_settings
end
# Ruta raíz
root 'home#index'
end# rails routes - Muestra todas las rutas generadas
#
# Verb URI Pattern Controller#Action
# GET /articles articles#index
# POST /articles articles#create
# GET /articles/new articles#new
# GET /articles/:id/edit articles#edit
# GET /articles/:id articles#show
# PATCH /articles/:id articles#update
# DELETE /articles/:id articles#destroy
# POST /articles/:id/publish articles#publish
# GET /articles/drafts articles#draftsLos helpers de ruta generados (article_path(@article), new_article_path) permiten referenciar las URL de forma dinámica y mantenible.
Pregunta 7: Explicar callbacks y filtros en los controllers
Los callbacks (before_action, after_action, around_action) permiten ejecutar código antes, después o alrededor de las acciones del controller.
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# Protección CSRF habilitada por defecto
protect_from_forgery with: :exception
# Callback global para autenticación
before_action :authenticate_user!
# Manejo global de errores
rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from ActionController::ParameterMissing, with: :bad_request
private
def not_found
render json: { error: 'Recurso no encontrado' }, status: :not_found
end
def bad_request(exception)
render json: { error: exception.message }, status: :bad_request
end
end# app/controllers/admin/products_controller.rb
class Admin::ProductsController < ApplicationController
# Callbacks con opciones
before_action :require_admin
before_action :set_product, only: [:show, :edit, :update, :destroy]
after_action :log_activity, only: [:create, :update, :destroy]
# Callback condicional
before_action :check_stock, only: [:update], if: :stock_changed?
def create
@product = Product.new(product_params)
if @product.save
redirect_to [:admin, @product], notice: 'Producto creado.'
else
render :new, status: :unprocessable_entity
end
end
def update
if @product.update(product_params)
redirect_to [:admin, @product], notice: 'Producto actualizado.'
else
render :edit, status: :unprocessable_entity
end
end
private
def require_admin
redirect_to root_path unless current_user&.admin?
end
def set_product
@product = Product.find(params[:id])
end
def stock_changed?
params[:product][:stock_quantity].present?
end
def log_activity
ActivityLog.create!(
user: current_user,
action: action_name,
resource: @product
)
end
def product_params
params.require(:product).permit(:name, :price, :description, :stock_quantity)
end
endLos callbacks se ejecutan en orden de declaración. Usar skip_before_action en las subclases para deshabilitar callbacks heredados. Evitar callbacks con demasiada lógica de negocio: preferir Service Objects.
Servicios y arquitectura
Pregunta 8: ¿Cómo implementar Service Objects en Rails?
Los Service Objects encapsulan lógica de negocio compleja que no pertenece ni a los Models ni a los Controllers. Mejoran la testabilidad y siguen el principio de responsabilidad única.
# app/services/order_processor.rb
# Service Object con interfaz estandarizada
class OrderProcessor
def initialize(order, payment_method:)
@order = order
@payment_method = payment_method
end
def call
return failure('Pedido ya procesado') if @order.processed?
ActiveRecord::Base.transaction do
validate_stock!
process_payment!
update_inventory!
send_confirmation!
@order.update!(status: 'completed', processed_at: Time.current)
end
success(@order)
rescue PaymentError => e
failure("Fallo en el pago: #{e.message}")
rescue InsufficientStockError => e
failure("Stock insuficiente: #{e.message}")
end
private
def validate_stock!
@order.items.each do |item|
unless item.product.stock_quantity >= item.quantity
raise InsufficientStockError, item.product.name
end
end
end
def process_payment!
result = PaymentGateway.charge(
amount: @order.total,
method: @payment_method,
description: "Pedido ##{@order.id}"
)
raise PaymentError, result.error unless result.success?
@order.update!(payment_reference: result.transaction_id)
end
def update_inventory!
@order.items.each do |item|
item.product.decrement!(:stock_quantity, item.quantity)
end
end
def send_confirmation!
OrderMailer.confirmation(@order).deliver_later
end
def success(data)
Result.new(success: true, data: data)
end
def failure(error)
Result.new(success: false, error: error)
end
Result = Struct.new(:success, :data, :error, keyword_init: true) do
def success? = success
def failure? = !success
end
end# app/controllers/orders_controller.rb
class OrdersController < ApplicationController
def create
@order = current_user.orders.build(order_params)
if @order.save
result = OrderProcessor.new(@order, payment_method: params[:payment_method]).call
if result.success?
redirect_to @order, notice: '¡Pedido confirmado!'
else
@order.update!(status: 'payment_failed')
flash.now[:alert] = result.error
render :new, status: :unprocessable_entity
end
else
render :new, status: :unprocessable_entity
end
end
endEl patrón Service Object sigue una convención simple: una clase, una responsabilidad, un método público call. Devolver un objeto Result permite manejar limpiamente éxito y fallo.
Pregunta 9: Explicar Concerns en Rails
Los Concerns permiten extraer y compartir código entre Models o Controllers. Utilizan ActiveSupport::Concern para una sintaxis de inclusión limpia.
# app/models/concerns/sluggable.rb
# Concern reutilizable para generar slugs
module Sluggable
extend ActiveSupport::Concern
included do
# Código ejecutado al incluir
before_validation :generate_slug, if: :should_generate_slug?
validates :slug, presence: true, uniqueness: true
end
# Métodos de clase
class_methods do
def find_by_slug!(slug)
find_by!(slug: slug)
end
def sluggable_source(column = :title)
@sluggable_source = column
end
def sluggable_source_column
@sluggable_source || :title
end
end
# Métodos de instancia
def to_param
slug
end
private
def should_generate_slug?
slug.blank? || send("#{self.class.sluggable_source_column}_changed?")
end
def generate_slug
source = send(self.class.sluggable_source_column)
return if source.blank?
base_slug = source.parameterize
self.slug = unique_slug(base_slug)
end
def unique_slug(base)
slug = base
counter = 1
while self.class.where(slug: slug).where.not(id: id).exists?
slug = "#{base}-#{counter}"
counter += 1
end
slug
end
end# app/models/article.rb
class Article < ApplicationRecord
include Sluggable
sluggable_source :title # Opcional, :title por defecto
end
# app/models/product.rb
class Product < ApplicationRecord
include Sluggable
sluggable_source :name
end# app/controllers/concerns/pagination.rb
# Concern para controllers
module Pagination
extend ActiveSupport::Concern
included do
helper_method :page_param, :per_page_param
end
private
def paginate(relation)
relation.page(page_param).per(per_page_param)
end
def page_param
params[:page]&.to_i || 1
end
def per_page_param
[params[:per_page]&.to_i || 25, 100].min
end
endLos Concerns son útiles para código verdaderamente compartido. Evitar crear Concerns solo para "acortar" un Model: eso oculta la complejidad sin reducirla.
Testing con RSpec
Pregunta 10: ¿Cómo estructurar los tests RSpec en Rails?
RSpec es el framework de testing estándar para Rails. Una buena estructura de tests incluye Model specs, Controller specs, Service specs y tests de integración.
# spec/models/user_spec.rb
require 'rails_helper'
RSpec.describe User, type: :model do
# Factories con FactoryBot
let(:user) { build(:user) }
let(:admin) { build(:user, :admin) }
describe 'validations' do
it { is_expected.to validate_presence_of(:email) }
it { is_expected.to validate_uniqueness_of(:email).case_insensitive }
it 'valida el formato del email' do
user.email = 'invalid'
expect(user).not_to be_valid
expect(user.errors[:email]).to include('is invalid')
end
end
describe 'associations' do
it { is_expected.to have_many(:articles).dependent(:destroy) }
it { is_expected.to have_one(:profile) }
it { is_expected.to belong_to(:organization).optional }
end
describe '#full_name' do
it 'devuelve el nombre y apellido combinados' do
user = build(:user, first_name: 'John', last_name: 'Doe')
expect(user.full_name).to eq('John Doe')
end
it 'maneja la ausencia de apellido' do
user = build(:user, first_name: 'John', last_name: nil)
expect(user.full_name).to eq('John')
end
end
describe '.active' do
it 'devuelve solo los usuarios activos' do
active = create(:user, active: true)
inactive = create(:user, active: false)
expect(User.active).to include(active)
expect(User.active).not_to include(inactive)
end
end
end# spec/services/order_processor_spec.rb
require 'rails_helper'
RSpec.describe OrderProcessor do
let(:user) { create(:user) }
let(:product) { create(:product, stock_quantity: 10, price: 100) }
let(:order) { create(:order, user: user, items: [build(:order_item, product: product, quantity: 2)]) }
subject { described_class.new(order, payment_method: 'card') }
describe '#call' do
context 'cuando el pedido es válido' do
before do
allow(PaymentGateway).to receive(:charge).and_return(
OpenStruct.new(success?: true, transaction_id: 'txn_123')
)
end
it 'procesa el pedido correctamente' do
result = subject.call
expect(result).to be_success
expect(order.reload.status).to eq('completed')
end
it 'decrementa el stock del producto' do
expect { subject.call }.to change { product.reload.stock_quantity }.by(-2)
end
it 'envía el correo de confirmación' do
expect { subject.call }
.to have_enqueued_mail(OrderMailer, :confirmation)
.with(order)
end
end
context 'cuando falla el pago' do
before do
allow(PaymentGateway).to receive(:charge).and_return(
OpenStruct.new(success?: false, error: 'Card declined')
)
end
it 'devuelve un resultado de fallo' do
result = subject.call
expect(result).to be_failure
expect(result.error).to include('Card declined')
end
it 'no actualiza el estado del pedido' do
expect { subject.call }.not_to change { order.reload.status }
end
end
end
end# spec/requests/api/v1/products_spec.rb
require 'rails_helper'
RSpec.describe 'API V1 Products', type: :request do
let(:user) { create(:user) }
let(:headers) { { 'Authorization' => "Bearer #{user.api_token}" } }
describe 'GET /api/v1/products' do
let!(:products) { create_list(:product, 3, :active) }
it 'devuelve la lista de productos' do
get '/api/v1/products', headers: headers
expect(response).to have_http_status(:ok)
expect(json_response['data'].size).to eq(3)
end
it 'filtra por categoría' do
category = create(:category)
categorized = create(:product, category: category)
get '/api/v1/products', params: { category_id: category.id }, headers: headers
expect(json_response['data'].map { |p| p['id'] }).to eq([categorized.id])
end
end
describe 'POST /api/v1/products' do
let(:valid_params) do
{ product: { name: 'Producto Nuevo', price: 99.99, category_id: create(:category).id } }
end
it 'crea un producto nuevo' do
expect {
post '/api/v1/products', params: valid_params, headers: headers
}.to change(Product, :count).by(1)
expect(response).to have_http_status(:created)
end
end
endBuenas prácticas: usar let para los datos, describe para métodos/contextos, context para condiciones, e it para aserciones específicas. Cada test debe probar una sola cosa.
Pregunta 11: ¿Cómo usar factories con FactoryBot?
FactoryBot permite crear datos de prueba de forma declarativa y mantenible. Las factories reemplazan a las fixtures estáticas.
# spec/factories/users.rb
FactoryBot.define do
factory :user do
# Secuencias para garantizar unicidad
sequence(:email) { |n| "user#{n}@example.com" }
first_name { Faker::Name.first_name }
last_name { Faker::Name.last_name }
password { 'password123' }
confirmed_at { Time.current }
# Traits para variaciones
trait :admin do
role { 'admin' }
after(:create) do |user|
user.permissions.create!(name: 'admin_access')
end
end
trait :unconfirmed do
confirmed_at { nil }
end
trait :with_profile do
after(:create) do |user|
create(:profile, user: user)
end
end
trait :with_articles do
transient do
articles_count { 3 }
end
after(:create) do |user, evaluator|
create_list(:article, evaluator.articles_count, author: user)
end
end
# Factory heredada
factory :admin_user do
admin
with_profile
end
end
end# spec/factories/orders.rb
FactoryBot.define do
factory :order do
user
status { 'pending' }
trait :with_items do
transient do
items_count { 2 }
end
after(:create) do |order, evaluator|
create_list(:order_item, evaluator.items_count, order: order)
order.recalculate_total!
end
end
trait :completed do
status { 'completed' }
processed_at { Time.current }
with_items
end
trait :high_value do
after(:create) do |order|
create(:order_item, order: order, quantity: 10, unit_price: 500)
order.recalculate_total!
end
end
end
end# Uso en tests
RSpec.describe OrderProcessor do
# build: instancia no persistida
let(:user) { build(:user) }
# create: persistida en BD
let(:order) { create(:order, :with_items, user: user) }
# create_list: múltiples instancias
let(:products) { create_list(:product, 5) }
# Combinar traits
let(:admin) { create(:user, :admin, :with_profile) }
# Sobrescribir atributos
let(:expensive_order) { create(:order, :with_items, items_count: 10) }
# build_stubbed: más rápido, para tests unitarios
let(:stubbed_user) { build_stubbed(:user) }
endPreferir build o build_stubbed sobre create cuando la persistencia no es necesaria: esto acelera significativamente los tests.
Background Jobs
Pregunta 12: ¿Cómo usar Active Job y Sidekiq en Rails?
Active Job ofrece una interfaz unificada para trabajos en segundo plano, sin importar el backend (Sidekiq, Resque, etc.). Sidekiq es la opción popular por su rendimiento con Redis.
# app/jobs/process_order_job.rb
class ProcessOrderJob < ApplicationJob
queue_as :default
# Configuración de reintentos
retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3
retry_on Net::OpenTimeout, wait: :polynomially_longer, attempts: 10
discard_on ActiveJob::DeserializationError
# Opciones de Sidekiq (si el backend es Sidekiq)
sidekiq_options retry: 5, backtrace: true
def perform(order_id)
order = Order.find(order_id)
OrderProcessor.new(order).call
rescue ActiveRecord::RecordNotFound
# Pedido eliminado entre el encolado y la ejecución
Rails.logger.warn("Order #{order_id} not found, skipping job")
end
end# app/jobs/batch_email_job.rb
class BatchEmailJob < ApplicationJob
queue_as :mailers
# Limitación de tasa con Sidekiq Enterprise o gem throttle
sidekiq_options throttle: { threshold: 100, period: 1.minute }
def perform(user_ids, template_id)
template = EmailTemplate.find(template_id)
User.where(id: user_ids).find_each do |user|
UserMailer.custom_email(user, template).deliver_later
end
end
end# Encolar trabajos
# Inmediato
ProcessOrderJob.perform_later(order.id)
# Diferido
ProcessOrderJob.set(wait: 5.minutes).perform_later(order.id)
# A una hora específica
ProcessOrderJob.set(wait_until: Date.tomorrow.noon).perform_later(order.id)
# Cola específica
ProcessOrderJob.set(queue: :critical).perform_later(order.id)
# Síncrono (para tests o depuración)
ProcessOrderJob.perform_now(order.id)# config/sidekiq.yml
:concurrency: 10
:queues:
- [critical, 3] # Alta prioridad, peso 3
- [default, 2] # Prioridad media, peso 2
- [mailers, 1] # Prioridad baja, peso 1
- [low, 1]
:schedule:
cleanup_job:
cron: '0 3 * * *' # Cada día a las 3am
class: CleanupJobActive Job abstrae el backend, pero acceder a funcionalidades específicas (batches, rate limiting) suele requerir acoplamiento con el backend elegido.
¿Listo para aprobar tus entrevistas de Ruby on Rails?
Practica con nuestros simuladores interactivos, flashcards y tests técnicos.
Desarrollo de API
Pregunta 13: ¿Cómo construir una API RESTful con Rails?
Rails facilita la construcción de APIs JSON con Controllers API-only y serializadores. Una buena API está versionada, documentada y es segura.
# app/controllers/api/v1/base_controller.rb
module Api
module V1
class BaseController < ActionController::API
include ActionController::HttpAuthentication::Token::ControllerMethods
before_action :authenticate_token!
rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from ActiveRecord::RecordInvalid, with: :unprocessable_entity
rescue_from ActionController::ParameterMissing, with: :bad_request
private
def authenticate_token!
authenticate_or_request_with_http_token do |token, options|
@current_user = User.find_by(api_token: token)
end
end
def current_user
@current_user
end
def not_found(exception)
render json: { error: 'Recurso no encontrado', details: exception.message },
status: :not_found
end
def unprocessable_entity(exception)
render json: { error: 'Validación fallida', details: exception.record.errors },
status: :unprocessable_entity
end
def bad_request(exception)
render json: { error: 'Petición incorrecta', details: exception.message },
status: :bad_request
end
end
end
end# app/controllers/api/v1/products_controller.rb
module Api
module V1
class ProductsController < BaseController
before_action :set_product, only: [:show, :update, :destroy]
def index
@products = Product.active
.includes(:category)
.page(params[:page])
.per(params[:per_page] || 20)
render json: {
data: ProductSerializer.new(@products).serializable_hash,
meta: pagination_meta(@products)
}
end
def show
render json: ProductSerializer.new(@product, include: [:category, :reviews])
end
def create
@product = Product.new(product_params)
@product.save!
render json: ProductSerializer.new(@product), status: :created
end
def update
@product.update!(product_params)
render json: ProductSerializer.new(@product)
end
def destroy
@product.destroy!
head :no_content
end
private
def set_product
@product = Product.find(params[:id])
end
def product_params
params.require(:product).permit(:name, :description, :price, :category_id)
end
def pagination_meta(collection)
{
current_page: collection.current_page,
total_pages: collection.total_pages,
total_count: collection.total_count
}
end
end
end
end# app/serializers/product_serializer.rb
# Con la gem jsonapi-serializer
class ProductSerializer
include JSONAPI::Serializer
attributes :id, :name, :description, :price, :created_at
attribute :formatted_price do |product|
"$#{product.price.to_f.round(2)}"
end
belongs_to :category
has_many :reviews
link :self do |product|
Rails.application.routes.url_helpers.api_v1_product_url(product)
end
endBuenas prácticas de API: versionar mediante namespace, usar códigos HTTP apropiados, paginar las colecciones y proporcionar mensajes de error claros.
Pregunta 14: ¿Cómo implementar autenticación JWT en Rails?
JWT (JSON Web Tokens) es un método de autenticación stateless popular para APIs. El token codifica la identidad y validez del usuario.
# app/services/jwt_service.rb
class JwtService
SECRET_KEY = Rails.application.credentials.secret_key_base
ALGORITHM = 'HS256'.freeze
class << self
def encode(payload, exp = 24.hours.from_now)
payload[:exp] = exp.to_i
payload[:iat] = Time.current.to_i
JWT.encode(payload, SECRET_KEY, ALGORITHM)
end
def decode(token)
decoded = JWT.decode(token, SECRET_KEY, true, algorithm: ALGORITHM)
HashWithIndifferentAccess.new(decoded.first)
rescue JWT::ExpiredSignature
raise AuthenticationError, 'Token expirado'
rescue JWT::DecodeError
raise AuthenticationError, 'Token inválido'
end
end
end# app/controllers/api/v1/auth_controller.rb
module Api
module V1
class AuthController < ActionController::API
def login
user = User.find_by(email: params[:email])
if user&.authenticate(params[:password])
token = JwtService.encode(user_id: user.id)
render json: {
token: token,
user: UserSerializer.new(user),
expires_at: 24.hours.from_now
}
else
render json: { error: 'Credenciales inválidas' }, status: :unauthorized
end
end
def refresh
token = JwtService.encode(user_id: current_user.id)
render json: { token: token, expires_at: 24.hours.from_now }
end
end
end
end# app/controllers/concerns/jwt_authenticatable.rb
module JwtAuthenticatable
extend ActiveSupport::Concern
included do
before_action :authenticate_jwt!
end
private
def authenticate_jwt!
header = request.headers['Authorization']
token = header&.split(' ')&.last
raise AuthenticationError, 'Token ausente' unless token
decoded = JwtService.decode(token)
@current_user = User.find(decoded[:user_id])
rescue AuthenticationError => e
render json: { error: e.message }, status: :unauthorized
rescue ActiveRecord::RecordNotFound
render json: { error: 'Usuario no encontrado' }, status: :unauthorized
end
def current_user
@current_user
end
endPara producción, considerar: refresh tokens, blacklisting de tokens al cerrar sesión y tiempos de expiración cortos. Gems como devise-jwt simplifican la implementación.
Caché y rendimiento
Pregunta 15: ¿Cómo implementar caché en Rails?
Rails ofrece varios niveles de caché: fragment caching, Russian Doll caching, low-level caching. La elección depende del caso de uso.
# config/environments/production.rb
config.action_controller.perform_caching = true
config.cache_store = :redis_cache_store, {
url: ENV['REDIS_URL'],
namespace: 'myapp:cache',
expires_in: 1.day,
race_condition_ttl: 10.seconds
}<%# app/views/products/index.html.erb %>
<%# Fragment caching con clave de caché automática %>
<% @products.each do |product| %>
<%# Caché basada en updated_at del producto %>
<% cache product do %>
<%= render product %>
<% end %>
<% end %>
<%# Russian Doll caching - caché anidada %>
<% cache ['v1', @category] do %>
<h2><%= @category.name %></h2>
<% @category.products.each do |product| %>
<% cache ['v1', product] do %>
<%= render product %>
<% end %>
<% end %>
<% end %>
<%# Caché condicional %>
<% cache_if current_user.nil?, @product do %>
<%= render @product %>
<% end %># app/models/product.rb
class Product < ApplicationRecord
# Touch al padre para invalidar la caché Russian Doll
belongs_to :category, touch: true
# Clave de caché personalizada
def cache_key_with_version
"#{super}/#{reviews.maximum(:updated_at)&.to_i}"
end
end# Low-level caching en servicios
class DashboardStatsService
def call
Rails.cache.fetch('dashboard:stats', expires_in: 15.minutes) do
{
total_users: User.count,
active_users: User.where('last_sign_in_at > ?', 30.days.ago).count,
total_orders: Order.completed.count,
revenue_mtd: Order.completed.where(created_at: Time.current.beginning_of_month..).sum(:total)
}
end
end
end
# Caché con protección contra race conditions
Rails.cache.fetch('popular_products', expires_in: 1.hour, race_condition_ttl: 10.seconds) do
Product.bestsellers.limit(10).to_a
end
# Invalidación explícita
Rails.cache.delete('dashboard:stats')
Rails.cache.delete_matched('products:*')Russian Doll caching es eficaz porque solo se regeneran los fragmentos modificados. Usar touch: true en las asociaciones para propagar la invalidación.
Pregunta 16: ¿Cómo optimizar el rendimiento de una aplicación Rails?
La optimización Rails cubre múltiples aspectos: consultas BD, caché, assets y arquitectura. Un enfoque metódico con monitorización es esencial.
# Optimización de la base de datos
# config/database.yml
production:
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
prepared_statements: true
advisory_locks: true
# app/models/order.rb
class Order < ApplicationRecord
# Índices compuestos para consultas frecuentes
# add_index :orders, [:user_id, :status, :created_at]
# Seleccionar solo las columnas necesarias
scope :summary, -> { select(:id, :status, :total, :created_at) }
# Procesamiento por lotes para grandes volúmenes
def self.process_pending
pending.find_each(batch_size: 1000) do |order|
ProcessOrderJob.perform_later(order.id)
end
end
# Evitar cálculos repetitivos
def self.revenue_by_month
completed
.group("DATE_TRUNC('month', created_at)")
.sum(:total)
end
end# Optimización de memoria
# config/puma.rb
workers ENV.fetch("WEB_CONCURRENCY") { 2 }
threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
threads threads_count, threads_count
preload_app!
before_fork do
ActiveRecord::Base.connection_pool.disconnect!
end
on_worker_boot do
ActiveRecord::Base.establish_connection
end# Profiling con rack-mini-profiler
# Gemfile
group :development do
gem 'rack-mini-profiler'
gem 'memory_profiler'
gem 'stackprof'
end
# config/initializers/mini_profiler.rb
if defined?(Rack::MiniProfiler)
Rack::MiniProfiler.config.position = 'bottom-right'
Rack::MiniProfiler.config.start_hidden = true
end# Lazy loading y paginación
class ProductsController < ApplicationController
def index
@products = Product.active
.includes(:category, :primary_image)
.page(params[:page])
.per(24)
# Prefetch para la siguiente página
if @products.next_page
Rails.cache.fetch("products:page:#{@products.next_page}", expires_in: 5.minutes) do
Product.active.page(@products.next_page).per(24).to_a
end
end
end
endHerramientas esenciales: rack-mini-profiler para profiling, bullet para detección de N+1, New Relic o Scout para monitorización en producción.
Seguridad
Pregunta 17: ¿Cuáles son las mejores prácticas de seguridad en Rails?
Rails incluye protecciones por defecto contra vulnerabilidades comunes. Comprender y configurar correctamente estas protecciones es crucial.
# Protección CSRF
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# Habilitada por defecto, lanza una excepción si el token es inválido
protect_from_forgery with: :exception
# Para APIs, usar :null_session
# protect_from_forgery with: :null_session
end
# En las vistas, el token se incluye automáticamente en los formularios
# <%= form_with ... %> incluye authenticity_token
# Para peticiones AJAX
# Añadir el header X-CSRF-Token con el valor de csrf_meta_tags# Prevención de inyección SQL
# ✅ Parámetros interpolados escapados automáticamente
User.where('email = ?', params[:email])
User.where(email: params[:email])
# ❌ PELIGRO - Interpolación directa
User.where("email = '#{params[:email]}'")
# ✅ Para cláusulas ORDER dinámicas
ALLOWED_SORTS = %w[name created_at price].freeze
sort_column = ALLOWED_SORTS.include?(params[:sort]) ? params[:sort] : 'name'
Product.order(sort_column)# Protección XSS
# Rails escapa automáticamente el HTML en las vistas
# ✅ Escapado automático
<%= user.name %>
# ❌ Peligroso - contenido sin escapar
<%== user.bio %>
<%= raw user.bio %>
<%= user.bio.html_safe %>
# ✅ Para HTML seguro, usar sanitize
<%= sanitize user.bio, tags: %w[p br strong em] %># Strong Parameters
class UsersController < ApplicationController
def update
@user.update!(user_params)
end
private
def user_params
# Whitelist explícita de atributos permitidos
params.require(:user).permit(:name, :email, :avatar)
# Solo para administradores
if current_user.admin?
params.require(:user).permit(:name, :email, :role, :active)
else
params.require(:user).permit(:name, :email)
end
end
end# Cabeceras de seguridad
# config/initializers/secure_headers.rb
Rails.application.config.action_dispatch.default_headers = {
'X-Frame-Options' => 'SAMEORIGIN',
'X-XSS-Protection' => '1; mode=block',
'X-Content-Type-Options' => 'nosniff',
'X-Download-Options' => 'noopen',
'X-Permitted-Cross-Domain-Policies' => 'none',
'Referrer-Policy' => 'strict-origin-when-cross-origin'
}
# Content Security Policy
Rails.application.config.content_security_policy do |policy|
policy.default_src :self
policy.script_src :self
policy.style_src :self, :unsafe_inline
policy.img_src :self, :data, 'https:'
endAuditar regularmente con brakeman (análisis estático de seguridad) y mantener las gems actualizadas con bundle audit.
Pregunta 18: ¿Cómo manejar autenticación y autorización en Rails?
La autenticación verifica la identidad, la autorización controla los permisos. Devise gestiona la auth, Pundit o CanCanCan gestionan la autorización.
# Configuración de Devise
# app/models/user.rb
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable,
:confirmable, :lockable, :trackable
enum role: { user: 0, moderator: 1, admin: 2 }
def admin?
role == 'admin'
end
end# Políticas Pundit
# app/policies/article_policy.rb
class ArticlePolicy < ApplicationPolicy
def index?
true
end
def show?
record.published? || owner_or_admin?
end
def create?
user.present?
end
def update?
owner_or_admin?
end
def destroy?
owner_or_admin?
end
def publish?
user&.admin? || user&.moderator?
end
# Scope para colecciones
class Scope < Scope
def resolve
if user&.admin?
scope.all
elsif user
scope.where(published: true).or(scope.where(author: user))
else
scope.where(published: true)
end
end
end
private
def owner_or_admin?
user&.admin? || record.author == user
end
end# Controller con Pundit
class ArticlesController < ApplicationController
include Pundit::Authorization
after_action :verify_authorized, except: :index
after_action :verify_policy_scoped, only: :index
def index
@articles = policy_scope(Article).includes(:author).page(params[:page])
end
def show
@article = Article.find(params[:id])
authorize @article
end
def update
@article = Article.find(params[:id])
authorize @article
if @article.update(article_params)
redirect_to @article, notice: 'Artículo actualizado.'
else
render :edit, status: :unprocessable_entity
end
end
def publish
@article = Article.find(params[:id])
authorize @article
@article.update!(published: true, published_at: Time.current)
redirect_to @article, notice: 'Artículo publicado.'
end
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
private
def user_not_authorized
flash[:alert] = "No estás autorizado para realizar esta acción."
redirect_back(fallback_location: root_path)
end
endPundit es más explícito y testeable que CanCanCan. Cada acción tiene un método de política correspondiente, y los scopes filtran automáticamente las colecciones.
Rails avanzado
Pregunta 19: Explicar el patrón Repository en Rails
El patrón Repository aísla la lógica de acceso a datos del resto de la aplicación. Aunque Rails usa Active Record (un patrón distinto), Repository puede ser útil para casos complejos.
# app/repositories/base_repository.rb
class BaseRepository
def initialize(model_class)
@model_class = model_class
end
def all
@model_class.all
end
def find(id)
@model_class.find(id)
end
def find_by(attributes)
@model_class.find_by(attributes)
end
def create(attributes)
@model_class.create(attributes)
end
def update(record, attributes)
record.update(attributes)
end
def delete(record)
record.destroy
end
end# app/repositories/product_repository.rb
class ProductRepository < BaseRepository
def initialize
super(Product)
end
def active
@model_class.where(active: true)
end
def in_category(category_id)
@model_class.where(category_id: category_id)
end
def search(query)
@model_class.where('name ILIKE ? OR description ILIKE ?',
"%#{query}%", "%#{query}%")
end
def with_stock
@model_class.where('stock_quantity > 0')
end
def bestsellers(limit: 10)
@model_class
.joins(:order_items)
.group(:id)
.order('COUNT(order_items.id) DESC')
.limit(limit)
end
def for_homepage
active
.with_stock
.includes(:category, :primary_image)
.order(featured: :desc, created_at: :desc)
.limit(12)
end
end# Uso en un servicio
class ProductSearchService
def initialize(repository: ProductRepository.new)
@repository = repository
end
def call(params)
products = @repository.active
products = products.in_category(params[:category]) if params[:category]
products = products.search(params[:query]) if params[:query].present?
products = products.with_stock if params[:in_stock]
products
end
end
# Facilita el testing con mocks
RSpec.describe ProductSearchService do
let(:repository) { instance_double(ProductRepository) }
let(:service) { described_class.new(repository: repository) }
it 'filtra por categoría' do
products = double('products')
allow(repository).to receive(:active).and_return(products)
allow(products).to receive(:in_category).with(1).and_return(products)
service.call(category: 1)
expect(products).to have_received(:in_category).with(1)
end
endRepository es opcional en Rails ya que Active Record es un excelente patrón. Usarlo para consultas complejas o cuando el aislamiento del almacenamiento sea importante.
Pregunta 20: ¿Cómo implementar el patrón CQRS en Rails?
CQRS (Command Query Responsibility Segregation) separa las operaciones de lectura y escritura. En Rails, esto se traduce en clases distintas para queries y commands.
# app/commands/base_command.rb
class BaseCommand
include ActiveModel::Validations
def self.call(*args)
new(*args).call
end
def call
return failure(errors) unless valid?
execute
end
private
def execute
raise NotImplementedError
end
def success(data = nil)
CommandResult.success(data)
end
def failure(errors)
CommandResult.failure(errors)
end
end
CommandResult = Struct.new(:success, :data, :errors, keyword_init: true) do
def success? = success
def failure? = !success
def self.success(data)
new(success: true, data: data, errors: [])
end
def self.failure(errors)
new(success: false, data: nil, errors: Array(errors))
end
end# app/commands/orders/create_order_command.rb
module Orders
class CreateOrderCommand < BaseCommand
attr_reader :user, :items, :shipping_address
validates :user, presence: true
validates :items, presence: true
validate :validate_items_availability
def initialize(user:, items:, shipping_address:)
@user = user
@items = items
@shipping_address = shipping_address
end
private
def execute
order = nil
ActiveRecord::Base.transaction do
order = Order.create!(
user: user,
shipping_address: shipping_address,
status: 'pending'
)
items.each do |item|
order.items.create!(
product_id: item[:product_id],
quantity: item[:quantity],
unit_price: Product.find(item[:product_id]).price
)
end
order.calculate_total!
end
OrderCreatedEvent.broadcast(order)
success(order)
rescue ActiveRecord::RecordInvalid => e
failure(e.message)
end
def validate_items_availability
items.each do |item|
product = Product.find_by(id: item[:product_id])
unless product&.stock_quantity&.>= item[:quantity]
errors.add(:items, "Producto #{item[:product_id]} no disponible")
end
end
end
end
end# app/queries/orders/user_orders_query.rb
module Orders
class UserOrdersQuery
def initialize(user, params = {})
@user = user
@params = params
end
def call
orders = @user.orders.includes(:items, items: :product)
orders = apply_status_filter(orders)
orders = apply_date_filter(orders)
orders = apply_sorting(orders)
orders.page(@params[:page]).per(@params[:per_page] || 20)
end
private
def apply_status_filter(orders)
return orders unless @params[:status]
orders.where(status: @params[:status])
end
def apply_date_filter(orders)
orders = orders.where('created_at >= ?', @params[:from]) if @params[:from]
orders = orders.where('created_at <= ?', @params[:to]) if @params[:to]
orders
end
def apply_sorting(orders)
case @params[:sort]
when 'oldest' then orders.order(created_at: :asc)
when 'total_desc' then orders.order(total: :desc)
else orders.order(created_at: :desc)
end
end
end
end# Controller usando CQRS
class OrdersController < ApplicationController
def index
@orders = Orders::UserOrdersQuery.new(current_user, filter_params).call
end
def create
result = Orders::CreateOrderCommand.call(
user: current_user,
items: order_params[:items],
shipping_address: order_params[:shipping_address]
)
if result.success?
redirect_to result.data, notice: '¡Pedido creado!'
else
flash.now[:alert] = result.errors.join(', ')
render :new, status: :unprocessable_entity
end
end
endCQRS brilla en aplicaciones complejas con necesidades asimétricas de lectura/escritura. Para CRUD simple, es sobre-ingeniería.
Pregunta 21: ¿Cómo manejar WebSockets con Action Cable?
Action Cable integra WebSockets en Rails para comunicación bidireccional en tiempo real. Usa Redis para la sincronización entre servidores.
# app/channels/application_cable/connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
end
private
def find_verified_user
# Vía cookie de sesión
if verified_user = User.find_by(id: cookies.encrypted[:user_id])
verified_user
# Vía JWT para APIs
elsif verified_user = verify_jwt_token
verified_user
else
reject_unauthorized_connection
end
end
def verify_jwt_token
token = request.params[:token]
return nil unless token
decoded = JwtService.decode(token)
User.find(decoded[:user_id])
rescue
nil
end
end
end# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
def subscribed
@room = ChatRoom.find(params[:room_id])
# Verificar permisos
unless @room.accessible_by?(current_user)
reject
return
end
stream_for @room
# Notificar a los demás de la presencia
broadcast_presence(:joined)
end
def unsubscribed
broadcast_presence(:left) if @room
end
def send_message(data)
message = @room.messages.create!(
user: current_user,
content: data['content']
)
# Difundir a todos los suscriptores
ChatChannel.broadcast_to(@room, {
type: 'message',
message: MessageSerializer.new(message).as_json
})
end
def typing
ChatChannel.broadcast_to(@room, {
type: 'typing',
user: current_user.name
})
end
private
def broadcast_presence(action)
ChatChannel.broadcast_to(@room, {
type: 'presence',
action: action,
user: current_user.name,
online_count: @room.online_users_count
})
end
endimport consumer from "./consumer"
const chatChannel = consumer.subscriptions.create(
{ channel: "ChatChannel", room_id: roomId },
{
connected() {
console.log("Connected to chat")
},
disconnected() {
console.log("Disconnected from chat")
},
received(data) {
switch(data.type) {
case 'message':
this.appendMessage(data.message)
break
case 'typing':
this.showTypingIndicator(data.user)
break
case 'presence':
this.updatePresence(data)
break
}
},
sendMessage(content) {
this.perform('send_message', { content: content })
},
notifyTyping() {
this.perform('typing')
}
}
)Action Cable maneja automáticamente las reconexiones y la sincronización. En producción, configurar Redis como adaptador y escalar según las conexiones concurrentes.
Pregunta 22: ¿Cómo implementar multi-tenancy en Rails?
La multi-tenancy permite que una aplicación sirva a múltiples clientes (tenants) aislados. Tres enfoques principales: a nivel de base de datos, a nivel de schema o a nivel de fila.
# Multitenancy a nivel de fila con ActsAsTenant o manual
# app/models/concerns/tenant_scoped.rb
module TenantScoped
extend ActiveSupport::Concern
included do
belongs_to :tenant
# Scope por defecto al tenant actual
default_scope -> { where(tenant: Current.tenant) if Current.tenant }
# Validación del tenant
before_validation :set_tenant, on: :create
end
private
def set_tenant
self.tenant ||= Current.tenant
end
end
# app/models/current.rb
class Current < ActiveSupport::CurrentAttributes
attribute :tenant, :user
end# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
before_action :set_current_tenant
private
def set_current_tenant
Current.tenant = resolve_tenant
Current.user = current_user
end
def resolve_tenant
# Vía subdominio
if request.subdomain.present? && request.subdomain != 'www'
Tenant.find_by!(subdomain: request.subdomain)
# Vía header (para APIs)
elsif request.headers['X-Tenant-ID'].present?
Tenant.find(request.headers['X-Tenant-ID'])
# Vía usuario
elsif current_user
current_user.tenant
end
rescue ActiveRecord::RecordNotFound
redirect_to root_url(subdomain: 'www'), alert: 'Tenant no encontrado'
end
end# app/models/project.rb
class Project < ApplicationRecord
include TenantScoped
has_many :tasks
belongs_to :owner, class_name: 'User'
end
# app/models/user.rb
class User < ApplicationRecord
include TenantScoped
has_many :projects, foreign_key: :owner_id
# Los administradores pueden pertenecer a varios tenants
has_many :tenant_memberships
has_many :accessible_tenants, through: :tenant_memberships, source: :tenant
end# A nivel de schema con la gem Apartment (PostgreSQL)
# config/initializers/apartment.rb
Apartment.configure do |config|
config.excluded_models = %w[Tenant User]
config.tenant_names = -> { Tenant.pluck(:subdomain) }
end
# Uso
Apartment::Tenant.switch('acme') do
# Todas las queries en este bloque usan el schema 'acme'
Project.all # SELECT * FROM acme.projects
endA nivel de fila es lo más simple pero requiere atención constante a las fugas. A nivel de schema ofrece mejor aislamiento pero complica las migraciones. Elegir según las necesidades de seguridad y escalabilidad.
Pregunta 23: ¿Cómo configurar una arquitectura de microservicios con Rails?
Rails puede servir como base para una arquitectura de microservicios con comunicación vía HTTP/gRPC o colas de mensajes. La clave es definir bien los límites.
# Cliente de servicio HTTP
# app/services/payment_service_client.rb
class PaymentServiceClient
include HTTParty
base_uri ENV.fetch('PAYMENT_SERVICE_URL')
def initialize
@options = {
headers: {
'Content-Type' => 'application/json',
'X-Service-Token' => ENV.fetch('SERVICE_TOKEN')
},
timeout: 10
}
end
def create_charge(amount:, currency:, source:, metadata: {})
response = self.class.post('/charges', @options.merge(
body: { amount: amount, currency: currency, source: source, metadata: metadata }.to_json
))
handle_response(response)
end
def get_charge(charge_id)
response = self.class.get("/charges/#{charge_id}", @options)
handle_response(response)
end
private
def handle_response(response)
case response.code
when 200..299
ServiceResult.success(response.parsed_response)
when 400..499
ServiceResult.failure(response.parsed_response['error'], code: response.code)
else
ServiceResult.failure('Servicio no disponible', code: response.code)
end
rescue Net::OpenTimeout, Net::ReadTimeout
ServiceResult.failure('Timeout del servicio')
end
end# Comunicación dirigida por eventos con Sidekiq/Redis
# app/events/order_events.rb
module OrderEvents
class Created
include Wisper::Publisher
def call(order)
broadcast(:order_created, order)
end
end
end
# app/listeners/inventory_listener.rb
class InventoryListener
def order_created(order)
order.items.each do |item|
InventoryServiceClient.new.reserve_stock(
product_id: item.product_id,
quantity: item.quantity,
reference: order.id
)
end
end
end
# config/initializers/wisper.rb
Wisper.subscribe(InventoryListener.new, async: true)
Wisper.subscribe(NotificationListener.new, async: true)# Patrón API Gateway
# app/controllers/api/v1/gateway_controller.rb
module Api
module V1
class GatewayController < BaseController
# Agregar múltiples servicios
def dashboard
results = Parallel.map([:orders, :inventory, :analytics], in_threads: 3) do |service|
fetch_from_service(service)
end
render json: {
orders: results[0],
inventory: results[1],
analytics: results[2]
}
end
private
def fetch_from_service(service)
case service
when :orders
OrderServiceClient.new.recent_orders(limit: 5)
when :inventory
InventoryServiceClient.new.low_stock_alerts
when :analytics
AnalyticsServiceClient.new.daily_summary
end
rescue => e
{ error: "#{service} no disponible", message: e.message }
end
end
end
endPara microservicios Rails: definir contratos de API claros (OpenAPI), implementar circuit breakers (gem circuitbox) y usar tracing distribuido (gem opentelemetry).
Pregunta 24: ¿Cómo desplegar una aplicación Rails en producción?
El despliegue moderno de Rails utiliza contenedores o PaaS. Una configuración robusta de producción cubre assets, base de datos y monitorización.
# config/environments/production.rb
Rails.application.configure do
config.cache_classes = true
config.eager_load = true
config.consider_all_requests_local = false
# Assets
config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
config.assets.compile = false
config.assets.digest = true
# Logging
config.log_level = ENV.fetch('LOG_LEVEL', 'info').to_sym
config.log_tags = [:request_id]
config.logger = ActiveSupport::Logger.new(STDOUT)
.tap { |logger| logger.formatter = Logger::Formatter.new }
.then { |logger| ActiveSupport::TaggedLogging.new(logger) }
# Caché
config.cache_store = :redis_cache_store, {
url: ENV['REDIS_URL'],
expires_in: 1.day
}
# Forzar SSL
config.force_ssl = true
config.ssl_options = { hsts: { subdomains: true } }
# Action Mailer
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
address: ENV['SMTP_HOST'],
port: ENV['SMTP_PORT'],
user_name: ENV['SMTP_USER'],
password: ENV['SMTP_PASSWORD'],
authentication: :plain,
enable_starttls_auto: true
}
end# Dockerfile
FROM ruby:3.3-alpine AS builder
RUN apk add --no-cache build-base postgresql-dev nodejs yarn
WORKDIR /app
COPY Gemfile Gemfile.lock ./
RUN bundle config set --local deployment true && \
bundle config set --local without 'development test' && \
bundle install
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile
COPY . .
RUN bundle exec rails assets:precompile
# Imagen de producción
FROM ruby:3.3-alpine
RUN apk add --no-cache postgresql-client tzdata
WORKDIR /app
COPY /app /app
COPY /usr/local/bundle /usr/local/bundle
ENV RAILS_ENV=production
ENV RAILS_LOG_TO_STDOUT=true
EXPOSE 3000
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]# docker-compose.production.yml
version: '3.8'
services:
web:
build: .
environment:
- DATABASE_URL=postgres://user:pass@db/app_production
- REDIS_URL=redis://redis:6379/0
- SECRET_KEY_BASE=${SECRET_KEY_BASE}
depends_on:
- db
- redis
deploy:
replicas: 3
resources:
limits:
memory: 512M
sidekiq:
build: .
command: bundle exec sidekiq
environment:
- DATABASE_URL=postgres://user:pass@db/app_production
- REDIS_URL=redis://redis:6379/0
depends_on:
- db
- redis
deploy:
replicas: 2
db:
image: postgres:15-alpine
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD=${DB_PASSWORD}
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
volumes:
postgres_data:
redis_data:Checklist de producción: SSL obligatorio, secretos vía ENV, health checks, backups automatizados de BD, monitorización (APM + logs + métricas) y alertas configuradas.
Pregunta 25: ¿Cuáles son las novedades de Rails 7+ que hay que conocer?
Rails 7+ trae cambios significativos: Hotwire por defecto, import maps, credenciales encriptadas mejoradas y muchas optimizaciones.
# Hotwire - Turbo Frames
# app/views/articles/index.html.erb
<%= turbo_frame_tag "articles" do %>
<% @articles.each do |article| %>
<%= turbo_frame_tag dom_id(article) do %>
<%= render article %>
<% end %>
<% end %>
<%= link_to "Cargar más", articles_path(page: @page + 1),
data: { turbo_frame: "articles" } %>
<% end %>
# Turbo Streams para actualizaciones en tiempo real
# app/controllers/comments_controller.rb
def create
@comment = @article.comments.create!(comment_params.merge(user: current_user))
respond_to do |format|
format.turbo_stream
format.html { redirect_to @article }
end
end
# app/views/comments/create.turbo_stream.erb
<%= turbo_stream.append "comments", @comment %>
<%= turbo_stream.update "comments_count", @article.comments.count %>
<%= turbo_stream.replace "comment_form", partial: "comments/form", locals: { comment: Comment.new } %># Controllers Stimulus
# app/javascript/controllers/search_controller.js
import { Controller } from "@hotwired/stimulus"
import { debounce } from "lodash-es"
export default class extends Controller {
static targets = ["input", "results"]
static values = { url: String }
connect() {
this.search = debounce(this.search.bind(this), 300)
}
async search() {
const query = this.inputTarget.value
if (query.length < 2) return
const response = await fetch(`${this.urlValue}?q=${encodeURIComponent(query)}`)
this.resultsTarget.innerHTML = await response.text()
}
}# Import Maps (sin bundler de JavaScript)
# config/importmap.rb
pin "application"
pin "@hotwired/turbo-rails", to: "turbo.min.js"
pin "@hotwired/stimulus", to: "stimulus.min.js"
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
pin_all_from "app/javascript/controllers", under: "controllers"
# Pins desde CDN
pin "lodash-es", to: "https://ga.jspm.io/npm:lodash-es@4.17.21/lodash.js"# Active Record Encryption (Rails 7+)
# app/models/user.rb
class User < ApplicationRecord
encrypts :email, deterministic: true # Permite búsquedas
encrypts :phone_number # No determinista por defecto
encrypts :ssn, deterministic: true, downcase: true
end
# config/credentials.yml.enc
active_record_encryption:
primary_key: abc123...
deterministic_key: def456...
key_derivation_salt: ghi789...# Mejoras en la interfaz de queries
# Rails 7.1+
# Queries asíncronas
users = User.where(active: true).load_async
# Continuar procesando mientras se ejecuta la query
# Acceder a los resultados con users.to_a
# Common Table Expressions (CTE)
User.with(
recent_orders: Order.where('created_at > ?', 30.days.ago)
).joins('JOIN recent_orders ON recent_orders.user_id = users.id')
# Detección automática de inverse_of
class Author < ApplicationRecord
has_many :books # inverse_of detectado automáticamente
end
# Strict loading por defecto (evita N+1)
class ApplicationRecord < ActiveRecord::Base
self.strict_loading_by_default = true
endRails 7+ favorece la simplicidad (sin Webpack por defecto) y HTML-over-the-wire con Hotwire. Este enfoque reduce la complejidad de JavaScript ofreciendo una experiencia de usuario moderna.
Conclusión
Las entrevistas de Ruby on Rails evalúan el dominio del framework completo y la comprensión de sus convenciones. Puntos clave a recordar:
✅ Fundamentos: MVC, Active Record, migraciones, validaciones y asociaciones
✅ Arquitectura: Service Objects, Concerns, Query Objects y patrones CQRS
✅ Rendimiento: queries N+1, caching (fragment, Russian Doll, low-level), eager loading
✅ Testing: RSpec, FactoryBot, request specs y buenas prácticas de testing
✅ Seguridad: CSRF, inyección SQL, XSS, Strong Parameters y autenticación/autorización
✅ APIs: diseño RESTful, JWT, serializadores y versionado
✅ Producción: background jobs, WebSockets, despliegue y monitorización
La filosofía Rails (Convention over Configuration, DRY y Rails Way) guía todas las decisiones arquitectónicas. Dominar estos principios y saber cuándo apartarse de ellos demuestra una experiencia sólida.
¡Empieza a practicar!
Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.
Etiquetas
Compartir
Artículos relacionados

ActiveRecord: Resolver problemas de consultas N+1 en Ruby on Rails
Guía completa para detectar y resolver consultas N+1 en Rails con ActiveRecord. Domina includes, preload, eager_load y herramientas de detección automática.

Ruby on Rails 7: Hotwire y Turbo para Aplicaciones Reactivas
Guia completa de Hotwire y Turbo en Rails 7. Aprende a construir aplicaciones reactivas sin escribir JavaScript con Turbo Drive, Frames y Streams.

Action Cable y WebSockets en Rails: Guía Completa para Entrevistas Técnicas
Guia completa sobre Action Cable y WebSockets en Ruby on Rails para entrevistas tecnicas. Conexiones, canales, Solid Cable, Turbo Streams, escalabilidad con Redis y testing con ejemplos de codigo.