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.

Preguntas de entrevista Ruby on Rails - Guía completa

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.

Consejo para la entrevista

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.

ruby
# 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
ruby
# 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
erb
<%# 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.

ruby
# 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
ruby
# 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)
end

Active 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.

ruby
# 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
ruby
# 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
bash
# 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, seed

Las 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.

ruby
# 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
ruby
# 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
ruby
# 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 cache

La 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.

ruby
# 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
ruby
# 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.

ruby
# 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
bash
# 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#drafts

Los 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.

ruby
# 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
ruby
# 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
end

Los 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.

ruby
# 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
ruby
# 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
end

El 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.

ruby
# 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
ruby
# 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
ruby
# 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
end

Los 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.

ruby
# 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
ruby
# 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
ruby
# 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
end

Buenas 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.

ruby
# 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
ruby
# 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
ruby
# 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) }
end

Preferir 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.

ruby
# 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
ruby
# 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
ruby
# 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)
ruby
# 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: CleanupJob

Active 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.

ruby
# 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
ruby
# 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
ruby
# 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
end

Buenas 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.

ruby
# 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
ruby
# 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
ruby
# 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
end

Para 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.

ruby
# 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
}
erb
<%# 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 %>
ruby
# 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
ruby
# 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.

ruby
# 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
ruby
# 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
ruby
# 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
ruby
# 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
end

Herramientas 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.

ruby
# 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
ruby
# 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)
ruby
# 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] %>
ruby
# 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
ruby
# 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:'
end

Auditar 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.

ruby
# 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
ruby
# 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
ruby
# 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
end

Pundit 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.

ruby
# 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
ruby
# 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
ruby
# 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
end

Repository 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.

ruby
# 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
ruby
# 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
ruby
# 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
ruby
# 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
end

CQRS 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.

ruby
# 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
ruby
# 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
end
app/javascript/channels/chat_channel.jsjavascript
import 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.

ruby
# 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
ruby
# 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
ruby
# 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
ruby
# 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
end

A 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.

ruby
# 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
ruby
# 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)
ruby
# 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
end

Para 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.

ruby
# 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
# 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 --from=builder /app /app
COPY --from=builder /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"]
yaml
# 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.

ruby
# 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 } %>
ruby
# 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()
  }
}
ruby
# 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"
ruby
# 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...
ruby
# 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
end

Rails 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

#ruby on rails
#ruby
#entrevista
#active record
#entrevista técnica

Compartir

Artículos relacionados