Domande colloquio Ruby on Rails: Top 25 nel 2026

Le 25 domande più frequenti per i colloqui Ruby on Rails. Architettura MVC, Active Record, migration, testing RSpec, API REST con risposte dettagliate ed esempi di codice.

Domande colloquio Ruby on Rails - Guida completa

I colloqui Ruby on Rails valutano la padronanza del framework Ruby più popolare, la comprensione dell'architettura MVC, dell'ORM Active Record e la capacità di costruire applicazioni web robuste seguendo la filosofia "Convention over Configuration". Questa guida copre le 25 domande più frequenti, dai fondamenti di Rails ai pattern avanzati di produzione.

Consiglio per il colloquio

I recruiter apprezzano i candidati che comprendono la filosofia Rails: "Convention over Configuration", DRY (Don't Repeat Yourself) e i pattern Rails Way. Spiegare perché Rails compie certe scelte architetturali fa la differenza.

Fondamenti di Ruby on Rails

Domanda 1: Spiegare il pattern MVC in Ruby on Rails

Il pattern Model-View-Controller (MVC) è il cuore architetturale di Rails. Separa le responsabilità in tre livelli distinti per una migliore manutenibilità e testabilità del codice.

ruby
# app/models/article.rb
# Il Model gestisce i dati e la logica di business
class Article < ApplicationRecord
  # Validazioni dei dati
  validates :title, presence: true, length: { minimum: 5 }
  validates :body, presence: true

  # Associazioni con altri modelli
  belongs_to :author, class_name: 'User'
  has_many :comments, dependent: :destroy
  has_many :tags, through: :article_tags

  # Scope per query riutilizzabili
  scope :published, -> { where(published: true) }
  scope :recent, -> { order(created_at: :desc).limit(10) }

  # Callback del ciclo di vita
  before_save :generate_slug

  private

  def generate_slug
    self.slug = title.parameterize if title_changed?
  end
end
ruby
# app/controllers/articles_controller.rb
# Il Controller riceve le richieste e orchestra la risposta
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: 'Articolo creato con successo.'
    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 visualizza i dati in formato HTML %>
<article class="article-detail">
  <header>
    <h1><%= @article.title %></h1>
    <p class="meta">
      Da <%= @article.author.name %>      <%= l @article.created_at, format: :long %>
    </p>
  </header>

  <div class="content">
    <%= simple_format @article.body %>
  </div>

  <%# Partial per i commenti %>
  <%= render @comments %>
</article>

Il flusso tipico: la richiesta arriva al Router, che la indirizza al Controller appropriato. Il Controller interagisce con il Model per recuperare o modificare i dati, poi passa questi dati alla View per il rendering HTML.

Domanda 2: Cos'è Active Record e come funziona l'ORM di Rails?

Active Record è l'ORM (Object-Relational Mapping) di Rails che implementa il pattern Active Record. Ogni classe Model rappresenta una tabella del database, e ogni istanza rappresenta una riga.

ruby
# app/models/user.rb
# Active Record mappa automaticamente le colonne agli attributi
class User < ApplicationRecord
  # La tabella 'users' è associata automaticamente
  # Colonne: id, email, name, created_at, updated_at

  has_secure_password # BCrypt per la password

  has_many :articles, foreign_key: :author_id
  has_one :profile, dependent: :destroy
  has_and_belongs_to_many :roles

  # Validazioni
  validates :email, presence: true,
                    uniqueness: { case_sensitive: false },
                    format: { with: URI::MailTo::EMAIL_REGEXP }

  # Callback
  before_save :normalize_email

  # Metodi di classe per le query
  def self.admins
    joins(:roles).where(roles: { name: 'admin' })
  end

  private

  def normalize_email
    self.email = email.downcase.strip
  end
end
ruby
# Esempi di query Active Record
# Console Rails o all'interno di un service

# Creazione
user = User.create!(email: 'dev@example.com', name: 'Alice', password: 'secret123')

# Lettura con condizioni
active_users = User.where(active: true).order(:name)
user = User.find_by(email: 'dev@example.com')

# Query concatenate (lazy evaluation)
recent_admins = User.admins
                    .where('created_at > ?', 1.month.ago)
                    .includes(:profile)
                    .limit(10)

# Prevenzione N+1 con eager loading
articles = Article.includes(:author, :comments).published

# Aggiornamento
user.update!(name: 'Alice Martin')

# Transazioni
User.transaction do
  user.debit_balance!(100)
  recipient.credit_balance!(100)
  Payment.create!(from: user, to: recipient, amount: 100)
end

Active Record converte i metodi Ruby in query SQL ottimizzate. Metodi come where, joins, includes sono lazy: la query viene eseguita solo durante l'iterazione o alla chiamata di to_a.

Domanda 3: Spiegare il sistema di migration di Rails

Le migration permettono di versionare lo schema del database con Ruby. Sono reversibili e consentono un'evoluzione controllata della struttura dei dati.

ruby
# db/migrate/20260203100000_create_products.rb
# Migration per creare una tabella
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 e updated_at automatici
    end

    # Indici per le performance
    add_index :products, :name
    add_index :products, [:category_id, :active]
  end
end
ruby
# db/migrate/20260203110000_add_slug_to_products.rb
# Migration per modificare una tabella esistente
class AddSlugToProducts < ActiveRecord::Migration[7.1]
  def change
    add_column :products, :slug, :string
    add_index :products, :slug, unique: true

    # Popolare gli slug esistenti
    reversible do |dir|
      dir.up do
        Product.find_each do |product|
          product.update_column(:slug, product.name.parameterize)
        end
      end
    end

    # Rendere non nullabile dopo il popolamento
    change_column_null :products, :slug, false
  end
end
bash
# Comandi essenziali per le migration
rails db:migrate              # Esegue le migration in sospeso
rails db:rollback             # Annulla l'ultima migration
rails db:rollback STEP=3      # Annulla le ultime 3 migration
rails db:migrate:status       # Visualizza lo stato delle migration
rails db:seed                 # Esegue db/seeds.rb
rails db:reset                # Drop, create, migrate, seed

Le migration devono essere reversibili. Il metodo change è intelligente e può invertire automaticamente le operazioni comuni. Per casi complessi, usare up e down separatamente.

Active Record avanzato

Domanda 4: Come ottimizzare le query N+1 in Rails?

Il problema N+1 si verifica quando una query iniziale è seguita da N query aggiuntive per caricare le associazioni. Rails offre diversi metodi di eager loading per risolvere questo problema.

ruby
# app/controllers/orders_controller.rb
class OrdersController < ApplicationController
  def index
    # ❌ PROBLEMA N+1: 1 query + N query per ordine
    # @orders = Order.all
    # Nella view: order.user.name genera una query per ordine

    # ✅ SOLUZIONE con includes (eager loading)
    @orders = Order.includes(:user, :items)
                   .where(status: 'completed')
                   .order(created_at: :desc)
    # Genera solo 3 query in totale
  end

  def show
    # includes: carica le associazioni separatamente (2-3 query)
    @order = Order.includes(items: :product).find(params[:id])

    # preload: forza il caricamento separato
    @order = Order.preload(:items, :user).find(params[:id])

    # eager_load: forza un LEFT OUTER JOIN (1 query)
    @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 di default
  scope :with_details, -> { includes(:user, items: :product) }

  # Counter cache per evitare query COUNT
  # Richiede: add_column :users, :orders_count, :integer, default: 0
  belongs_to :user, counter_cache: true
end
ruby
# Rilevamento N+1 con la gem Bullet (sviluppo)
# config/environments/development.rb
config.after_initialize do
  Bullet.enable = true
  Bullet.alert = true
  Bullet.bullet_logger = true
  Bullet.rails_logger = true
end

# Bullet mostrerà avvisi quando:
# - Viene rilevata una query N+1
# - È presente eager loading non necessario
# - Dovrebbe essere usato un counter cache

La regola: usare includes di default (Rails sceglie la strategia ottimale), preload quando si vogliono forzare query separate, eager_load quando si filtra sulle associazioni.

Domanda 5: Spiegare Scope e Query Object in Rails

Gli scope incapsulano condizioni di query riutilizzabili. Per query complesse, i Query Object offrono migliore organizzazione e testabilità.

ruby
# app/models/product.rb
class Product < ApplicationRecord
  # Scope semplici
  scope :active, -> { where(active: true) }
  scope :in_stock, -> { where('stock_quantity > 0') }
  scope :featured, -> { where(featured: true) }

  # Scope con parametri
  scope :cheaper_than, ->(price) { where('price < ?', price) }
  scope :in_category, ->(category) { where(category: category) }

  # Scope concatenabili
  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 subquery
  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 per ricerche complesse
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

# Utilizzo nel controller
@products = ProductsSearchQuery.new(Product.active).call(params)

Gli scope sono perfetti per condizioni semplici e riutilizzabili. I Query Object si adattano a ricerche complesse con più filtri opzionali e logica di composizione.

Pronto a superare i tuoi colloqui su Ruby on Rails?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

Routing e Controller

Domanda 6: Come funziona il routing RESTful in Rails?

Rails incoraggia rotte RESTful che mappano i verbi HTTP alle azioni CRUD. Il router traduce gli URL in chiamate specifiche al controller.

ruby
# config/routes.rb
Rails.application.routes.draw do
  # Rotte RESTful standard (7 azioni)
  resources :articles do
    # Rotte annidate
    resources :comments, only: [:create, :destroy]

    # Rotte member (agiscono su un'istanza)
    member do
      post :publish
      delete :archive
    end

    # Rotte collection (agiscono sulla collezione)
    collection do
      get :drafts
      get :search
    end
  end

  # Rotte API con namespace
  namespace :api do
    namespace :v1 do
      resources :products, only: [:index, :show, :create, :update] do
        resources :reviews, shallow: true
      end
    end
  end

  # Rotta personalizzata
  get 'dashboard', to: 'dashboard#index'

  # Vincoli sulle rotte
  constraints(SubdomainConstraint.new) do
    resources :admin_settings
  end

  # Rotta principale
  root 'home#index'
end
bash
# rails routes - Mostra tutte le rotte generate
#
# 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

Gli helper di rotta generati (article_path(@article), new_article_path) permettono di referenziare gli URL in modo dinamico e manutenibile.

Domanda 7: Spiegare callback e filter nei controller

I callback (before_action, after_action, around_action) permettono di eseguire codice prima, dopo o intorno alle azioni del controller.

ruby
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  # Protezione CSRF abilitata di default
  protect_from_forgery with: :exception

  # Callback globale per l'autenticazione
  before_action :authenticate_user!

  # Gestione globale degli errori
  rescue_from ActiveRecord::RecordNotFound, with: :not_found
  rescue_from ActionController::ParameterMissing, with: :bad_request

  private

  def not_found
    render json: { error: 'Risorsa non trovata' }, 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
  # Callback con opzioni
  before_action :require_admin
  before_action :set_product, only: [:show, :edit, :update, :destroy]
  after_action :log_activity, only: [:create, :update, :destroy]

  # Callback condizionale
  before_action :check_stock, only: [:update], if: :stock_changed?

  def create
    @product = Product.new(product_params)

    if @product.save
      redirect_to [:admin, @product], notice: 'Prodotto creato.'
    else
      render :new, status: :unprocessable_entity
    end
  end

  def update
    if @product.update(product_params)
      redirect_to [:admin, @product], notice: 'Prodotto aggiornato.'
    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

I callback vengono eseguiti nell'ordine di dichiarazione. Usare skip_before_action nelle sottoclassi per disabilitare callback ereditati. Evitare callback con troppa logica di business: preferire i Service Object.

Service e architettura

Domanda 8: Come implementare i Service Object in Rails?

I Service Object incapsulano logica di business complessa che non appartiene né ai Model né ai Controller. Migliorano la testabilità e seguono il principio di singola responsabilità.

ruby
# app/services/order_processor.rb
# Service Object con interfaccia standardizzata
class OrderProcessor
  def initialize(order, payment_method:)
    @order = order
    @payment_method = payment_method
  end

  def call
    return failure('Ordine già elaborato') 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("Pagamento fallito: #{e.message}")
  rescue InsufficientStockError => e
    failure("Stock insufficiente: #{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: "Ordine ##{@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: 'Ordine confermato!'
      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

Il pattern Service Object segue una convenzione semplice: una classe, una responsabilità, un metodo pubblico call. Restituire un oggetto Result permette una gestione pulita di successo e fallimento.

Domanda 9: Spiegare i Concern in Rails

I Concern permettono di estrarre e condividere codice tra Model o Controller. Utilizzano ActiveSupport::Concern per una sintassi di inclusione pulita.

ruby
# app/models/concerns/sluggable.rb
# Concern riutilizzabile per generare slug
module Sluggable
  extend ActiveSupport::Concern

  included do
    # Codice eseguito all'inclusione
    before_validation :generate_slug, if: :should_generate_slug?
    validates :slug, presence: true, uniqueness: true
  end

  # Metodi di classe
  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

  # Metodi di istanza
  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 # Opzionale, :title di default
end

# app/models/product.rb
class Product < ApplicationRecord
  include Sluggable

  sluggable_source :name
end
ruby
# app/controllers/concerns/pagination.rb
# Concern per i controller
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

I Concern sono utili per codice realmente condiviso. Evitare di creare Concern solo per "accorciare" un Model: ciò nasconde la complessità senza ridurla.

Testing con RSpec

Domanda 10: Come strutturare i test RSpec in Rails?

RSpec è il framework di testing standard per Rails. Una buona struttura di test include Model spec, Controller spec, Service spec e test di integrazione.

ruby
# spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do
  # Factory 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 il formato dell’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 'restituisce nome e cognome combinati' do
      user = build(:user, first_name: 'John', last_name: 'Doe')
      expect(user.full_name).to eq('John Doe')
    end

    it 'gestisce il cognome mancante' do
      user = build(:user, first_name: 'John', last_name: nil)
      expect(user.full_name).to eq('John')
    end
  end

  describe '.active' do
    it 'restituisce solo gli utenti attivi' 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 'quando l’ordine è valido' do
      before do
        allow(PaymentGateway).to receive(:charge).and_return(
          OpenStruct.new(success?: true, transaction_id: 'txn_123')
        )
      end

      it 'elabora l’ordine con successo' do
        result = subject.call

        expect(result).to be_success
        expect(order.reload.status).to eq('completed')
      end

      it 'decrementa lo stock del prodotto' do
        expect { subject.call }.to change { product.reload.stock_quantity }.by(-2)
      end

      it 'invia l’email di conferma' do
        expect { subject.call }
          .to have_enqueued_mail(OrderMailer, :confirmation)
          .with(order)
      end
    end

    context 'quando il pagamento fallisce' do
      before do
        allow(PaymentGateway).to receive(:charge).and_return(
          OpenStruct.new(success?: false, error: 'Card declined')
        )
      end

      it 'restituisce un risultato di fallimento' do
        result = subject.call

        expect(result).to be_failure
        expect(result.error).to include('Card declined')
      end

      it 'non aggiorna lo stato dell’ordine' 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 'restituisce la lista dei prodotti' 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 per categoria' 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: 'Nuovo Prodotto', price: 99.99, category_id: create(:category).id } }
    end

    it 'crea un nuovo prodotto' 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

Buone pratiche: usare let per i dati, describe per metodi/contesti, context per le condizioni e it per le asserzioni specifiche. Un test dovrebbe testare una sola cosa.

Domanda 11: Come usare le factory con FactoryBot?

FactoryBot permette di creare dati di test in modo dichiarativo e manutenibile. Le factory sostituiscono le fixture statiche.

ruby
# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    # Sequenze per garantire l'unicità
    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 }

    # Trait per le variazioni
    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 ereditata
    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
# Utilizzo nei test
RSpec.describe OrderProcessor do
  # build: istanza non persistita
  let(:user) { build(:user) }

  # create: persistita su DB
  let(:order) { create(:order, :with_items, user: user) }

  # create_list: più istanze
  let(:products) { create_list(:product, 5) }

  # Combinare i trait
  let(:admin) { create(:user, :admin, :with_profile) }

  # Sovrascrivere gli attributi
  let(:expensive_order) { create(:order, :with_items, items_count: 10) }

  # build_stubbed: più veloce, per i test unitari
  let(:stubbed_user) { build_stubbed(:user) }
end

Preferire build o build_stubbed rispetto a create quando la persistenza non è necessaria: questo accelera significativamente i test.

Background Job

Domanda 12: Come usare Active Job e Sidekiq in Rails?

Active Job offre un'interfaccia unificata per i job in background, indipendentemente dal backend (Sidekiq, Resque, ecc.). Sidekiq è la scelta più popolare per le sue performance con Redis.

ruby
# app/jobs/process_order_job.rb
class ProcessOrderJob < ApplicationJob
  queue_as :default

  # Configurazione dei retry
  retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3
  retry_on Net::OpenTimeout, wait: :polynomially_longer, attempts: 10
  discard_on ActiveJob::DeserializationError

  # Opzioni Sidekiq (se backend Sidekiq)
  sidekiq_options retry: 5, backtrace: true

  def perform(order_id)
    order = Order.find(order_id)

    OrderProcessor.new(order).call
  rescue ActiveRecord::RecordNotFound
    # Ordine eliminato tra l'accodamento e l'esecuzione
    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

  # Limitazione del rate 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
# Accodare i job
# Immediato
ProcessOrderJob.perform_later(order.id)

# Ritardato
ProcessOrderJob.set(wait: 5.minutes).perform_later(order.id)

# A un orario specifico
ProcessOrderJob.set(wait_until: Date.tomorrow.noon).perform_later(order.id)

# Coda specifica
ProcessOrderJob.set(queue: :critical).perform_later(order.id)

# Sincrono (per test o debug)
ProcessOrderJob.perform_now(order.id)
ruby
# config/sidekiq.yml
:concurrency: 10
:queues:
  - [critical, 3]    # Alta priorità, peso 3
  - [default, 2]     # Media priorità, peso 2
  - [mailers, 1]     # Bassa priorità, peso 1
  - [low, 1]

:schedule:
  cleanup_job:
    cron: '0 3 * * *'  # Ogni giorno alle 3:00
    class: CleanupJob

Active Job astrae il backend, ma accedere a funzionalità specifiche (batch, rate limiting) spesso richiede accoppiamento con il backend scelto.

Pronto a superare i tuoi colloqui su Ruby on Rails?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

Sviluppo API

Domanda 13: Come costruire un'API RESTful con Rails?

Rails facilita la costruzione di API JSON con controller API-only e serializer. Una buona API è versionata, documentata e sicura.

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: 'Risorsa non trovata', details: exception.message },
               status: :not_found
      end

      def unprocessable_entity(exception)
        render json: { error: 'Validazione fallita', details: exception.record.errors },
               status: :unprocessable_entity
      end

      def bad_request(exception)
        render json: { error: 'Richiesta non valida', 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

Buone pratiche per le API: versionare tramite namespace, usare codici HTTP appropriati, paginare le collezioni e fornire messaggi di errore chiari.

Domanda 14: Come implementare l'autenticazione JWT in Rails?

JWT (JSON Web Tokens) è un metodo di autenticazione stateless popolare per le API. Il token codifica l'identità e la validità dell'utente.

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 scaduto'
    rescue JWT::DecodeError
      raise AuthenticationError, 'Token non valido'
    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: 'Credenziali non valide' }, 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 assente' 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: 'Utente non trovato' }, status: :unauthorized
  end

  def current_user
    @current_user
  end
end

Per la produzione, considerare: refresh token, blacklisting dei token al logout e tempi di scadenza brevi. Gem come devise-jwt semplificano l'implementazione.

Cache e performance

Domanda 15: Come implementare il caching in Rails?

Rails offre diversi livelli di caching: fragment caching, Russian Doll caching, low-level caching. La scelta dipende dal caso d'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 chiave di cache automatica %>
<% @products.each do |product| %>
  <%# Cache basata su updated_at del prodotto %>
  <% cache product do %>
    <%= render product %>
  <% end %>
<% end %>

<%# Russian Doll caching - cache annidata %>
<% cache ['v1', @category] do %>
  <h2><%= @category.name %></h2>

  <% @category.products.each do |product| %>
    <% cache ['v1', product] do %>
      <%= render product %>
    <% end %>
  <% end %>
<% end %>

<%# Cache condizionale %>
<% cache_if current_user.nil?, @product do %>
  <%= render @product %>
<% end %>
ruby
# app/models/product.rb
class Product < ApplicationRecord
  # Touch sul padre per invalidare la cache Russian Doll
  belongs_to :category, touch: true

  # Chiave di cache personalizzata
  def cache_key_with_version
    "#{super}/#{reviews.maximum(:updated_at)&.to_i}"
  end
end
ruby
# Low-level caching nei service
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

# Cache con protezione contro race condition
Rails.cache.fetch('popular_products', expires_in: 1.hour, race_condition_ttl: 10.seconds) do
  Product.bestsellers.limit(10).to_a
end

# Invalidazione esplicita
Rails.cache.delete('dashboard:stats')
Rails.cache.delete_matched('products:*')

Il Russian Doll caching è efficace perché solo i frammenti modificati vengono rigenerati. Usare touch: true sulle associazioni per propagare l'invalidazione.

Domanda 16: Come ottimizzare le performance di un'applicazione Rails?

L'ottimizzazione di Rails copre diversi aspetti: query DB, cache, asset e architettura. Un approccio metodico con monitoraggio è essenziale.

ruby
# Ottimizzazione del database
# config/database.yml
production:
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  prepared_statements: true
  advisory_locks: true

# app/models/order.rb
class Order < ApplicationRecord
  # Indici composti per query frequenti
  # add_index :orders, [:user_id, :status, :created_at]

  # Selezionare solo le colonne necessarie
  scope :summary, -> { select(:id, :status, :total, :created_at) }

  # Elaborazione batch per grandi volumi
  def self.process_pending
    pending.find_each(batch_size: 1000) do |order|
      ProcessOrderJob.perform_later(order.id)
    end
  end

  # Evitare calcoli ripetitivi
  def self.revenue_by_month
    completed
      .group("DATE_TRUNC('month', created_at)")
      .sum(:total)
  end
end
ruby
# Ottimizzazione della 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 e paginazione
class ProductsController < ApplicationController
  def index
    @products = Product.active
                       .includes(:category, :primary_image)
                       .page(params[:page])
                       .per(24)

    # Prefetch per la pagina successiva
    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

Strumenti essenziali: rack-mini-profiler per il profiling, bullet per la rilevazione di N+1, New Relic o Scout per il monitoraggio in produzione.

Sicurezza

Domanda 17: Quali sono le best practice di sicurezza in Rails?

Rails include protezioni di default contro le vulnerabilità più comuni. Comprendere e configurare correttamente queste protezioni è cruciale.

ruby
# Protezione CSRF
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  # Abilitata di default, solleva un'eccezione se il token non è valido
  protect_from_forgery with: :exception

  # Per le API, usare :null_session
  # protect_from_forgery with: :null_session
end

# Nelle view, il token è incluso automaticamente nei form
# <%= form_with ... %> include authenticity_token

# Per le richieste AJAX
# Aggiungere l'header X-CSRF-Token con il valore di csrf_meta_tags
ruby
# Prevenzione SQL Injection
# ✅ Parametri interpolati con escape automatico
User.where('email = ?', params[:email])
User.where(email: params[:email])

# ❌ PERICOLO - Interpolazione diretta
User.where("email = '#{params[:email]}'")

# ✅ Per clausole ORDER dinamiche
ALLOWED_SORTS = %w[name created_at price].freeze
sort_column = ALLOWED_SORTS.include?(params[:sort]) ? params[:sort] : 'name'
Product.order(sort_column)
ruby
# Protezione XSS
# Rails fa l'escape dell'HTML nelle view automaticamente

# ✅ Escape automatico
<%= user.name %>

# ❌ Pericoloso - contenuto senza escape
<%== user.bio %>
<%= raw user.bio %>
<%= user.bio.html_safe %>

# ✅ Per HTML sicuro, usare 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 esplicita degli attributi consentiti
    params.require(:user).permit(:name, :email, :avatar)

    # Solo per amministratori
    if current_user.admin?
      params.require(:user).permit(:name, :email, :role, :active)
    else
      params.require(:user).permit(:name, :email)
    end
  end
end
ruby
# Header di sicurezza
# 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

Auditare regolarmente con brakeman (analisi statica di sicurezza) e mantenere aggiornate le gem con bundle audit.

Domanda 18: Come gestire autenticazione e autorizzazione in Rails?

L'autenticazione verifica l'identità, l'autorizzazione controlla i permessi. Devise gestisce l'auth, Pundit o CanCanCan gestiscono l'autorizzazione.

ruby
# Setup di 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
# Policy 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 per le collezioni
  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: 'Articolo aggiornato.'
    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: 'Articolo pubblicato.'
  end

  rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized

  private

  def user_not_authorized
    flash[:alert] = "Non sei autorizzato a eseguire questa azione."
    redirect_back(fallback_location: root_path)
  end
end

Pundit è più esplicito e testabile rispetto a CanCanCan. Ogni azione ha un metodo di policy corrispondente, e gli scope filtrano automaticamente le collezioni.

Rails avanzato

Domanda 19: Spiegare il pattern Repository in Rails

Il pattern Repository isola la logica di accesso ai dati dal resto dell'applicazione. Sebbene Rails usi Active Record (un pattern diverso), Repository può essere utile per casi complessi.

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
# Utilizzo in un service
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 il testing con i mock
RSpec.describe ProductSearchService do
  let(:repository) { instance_double(ProductRepository) }
  let(:service) { described_class.new(repository: repository) }

  it 'filtra per categoria' 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 è opzionale in Rails poiché Active Record è già un eccellente pattern. Usarlo per query complesse o quando l'isolamento dello storage è importante.

Domanda 20: Come implementare il pattern CQRS in Rails?

CQRS (Command Query Responsibility Segregation) separa le operazioni di lettura e scrittura. In Rails, ciò si traduce in classi distinte per query e command.

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, "Prodotto #{item[:product_id]} non disponibile")
        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 che usa 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: 'Ordine creato!'
    else
      flash.now[:alert] = result.errors.join(', ')
      render :new, status: :unprocessable_entity
    end
  end
end

CQRS brilla in applicazioni complesse con esigenze asimmetriche di lettura/scrittura. Per CRUD semplici, è over-engineering.

Domanda 21: Come gestire i WebSocket con Action Cable?

Action Cable integra i WebSocket in Rails per la comunicazione bidirezionale in tempo reale. Utilizza Redis per la sincronizzazione tra server.

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
      # Tramite cookie di sessione
      if verified_user = User.find_by(id: cookies.encrypted[:user_id])
        verified_user
      # Tramite JWT per le API
      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])

    # Verificare i permessi
    unless @room.accessible_by?(current_user)
      reject
      return
    end

    stream_for @room

    # Notificare gli altri della presenza
    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']
    )

    # Trasmettere a tutti gli iscritti
    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 gestisce automaticamente le riconnessioni e la sincronizzazione. In produzione, configurare Redis come adapter e scalare in base alle connessioni concorrenti.

Domanda 22: Come implementare la multi-tenancy in Rails?

La multi-tenancy permette a un'applicazione di servire più client (tenant) isolati. Tre approcci principali: a livello di database, a livello di schema o a livello di riga.

ruby
# Multitenancy a livello di riga con ActsAsTenant o manuale
# app/models/concerns/tenant_scoped.rb
module TenantScoped
  extend ActiveSupport::Concern

  included do
    belongs_to :tenant

    # Scope di default sul tenant corrente
    default_scope -> { where(tenant: Current.tenant) if Current.tenant }

    # Validazione 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
    # Tramite sottodominio
    if request.subdomain.present? && request.subdomain != 'www'
      Tenant.find_by!(subdomain: request.subdomain)
    # Tramite header (per le API)
    elsif request.headers['X-Tenant-ID'].present?
      Tenant.find(request.headers['X-Tenant-ID'])
    # Tramite utente
    elsif current_user
      current_user.tenant
    end
  rescue ActiveRecord::RecordNotFound
    redirect_to root_url(subdomain: 'www'), alert: 'Tenant non trovato'
  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

  # Gli amministratori possono appartenere a più tenant
  has_many :tenant_memberships
  has_many :accessible_tenants, through: :tenant_memberships, source: :tenant
end
ruby
# A livello di 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

# Utilizzo
Apartment::Tenant.switch('acme') do
  # Tutte le query in questo blocco usano lo schema 'acme'
  Project.all # SELECT * FROM acme.projects
end

Il livello di riga è il più semplice ma richiede attenzione costante alle perdite. Il livello di schema offre un isolamento migliore ma complica le migration. Scegliere in base alle esigenze di sicurezza e scalabilità.

Domanda 23: Come configurare un'architettura a microservizi con Rails?

Rails può servire come base per un'architettura a microservizi con comunicazione tramite HTTP/gRPC o code di messaggi. La chiave è definire bene i confini.

ruby
# Client di servizio 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('Servizio non disponibile', code: response.code)
    end
  rescue Net::OpenTimeout, Net::ReadTimeout
    ServiceResult.failure('Timeout del servizio')
  end
end
ruby
# Comunicazione event-driven 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
# Pattern API Gateway
# app/controllers/api/v1/gateway_controller.rb
module Api
  module V1
    class GatewayController < BaseController
      # Aggregare più servizi
      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} non disponibile", message: e.message }
      end
    end
  end
end

Per i microservizi Rails: definire contratti API chiari (OpenAPI), implementare circuit breaker (gem circuitbox) e usare il tracing distribuito (gem opentelemetry).

Domanda 24: Come fare il deploy di un'applicazione Rails in produzione?

Il deploy moderno di Rails utilizza container o PaaS. Una configurazione di produzione robusta copre asset, database e monitoraggio.

ruby
# config/environments/production.rb
Rails.application.configure do
  config.cache_classes = true
  config.eager_load = true
  config.consider_all_requests_local = false

  # Asset
  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) }

  # Cache
  config.cache_store = :redis_cache_store, {
    url: ENV['REDIS_URL'],
    expires_in: 1.day
  }

  # Forzare 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

# Immagine di produzione
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 di produzione: SSL obbligatorio, segreti tramite ENV, health check, backup automatici del DB, monitoraggio (APM + log + metriche) e alerting configurato.

Domanda 25: Quali sono le novità di Rails 7+ da conoscere?

Rails 7+ porta cambiamenti significativi: Hotwire di default, import map, credenziali criptate migliorate e numerose ottimizzazioni.

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 "Carica altri", articles_path(page: @page + 1),
              data: { turbo_frame: "articles" } %>
<% end %>

# Turbo Streams per gli aggiornamenti in tempo reale
# 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
# Controller 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 (senza bundler 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"

# Pin da 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  # Permette le ricerche
  encrypts :phone_number                 # Non deterministico di default
  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
# Miglioramenti all'interfaccia di query
# Rails 7.1+

# Query asincrone
users = User.where(active: true).load_async
# Continuare a elaborare mentre la query è in esecuzione
# Accedere ai risultati 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')

# Rilevamento automatico di inverse_of
class Author < ApplicationRecord
  has_many :books # inverse_of rilevato automaticamente
end

# Strict loading di default (evita N+1)
class ApplicationRecord < ActiveRecord::Base
  self.strict_loading_by_default = true
end

Rails 7+ favorisce la semplicità (niente Webpack di default) e l'HTML-over-the-wire con Hotwire. Questo approccio riduce la complessità di JavaScript offrendo al contempo un'esperienza utente moderna.

Conclusione

I colloqui Ruby on Rails valutano la padronanza dell'intero framework e la comprensione delle sue convenzioni. Punti chiave da ricordare:

Fondamenti: MVC, Active Record, migration, validazioni e associazioni

Architettura: Service Object, Concern, Query Object e pattern CQRS

Performance: query N+1, caching (fragment, Russian Doll, low-level), eager loading

Testing: RSpec, FactoryBot, request spec e best practice di testing

Sicurezza: CSRF, SQL injection, XSS, Strong Parameters e autenticazione/autorizzazione

API: design RESTful, JWT, serializer e versionamento

Produzione: background job, WebSocket, deploy e monitoraggio

La filosofia Rails (Convention over Configuration, DRY e Rails Way) guida tutte le decisioni architetturali. Padroneggiare questi principi e sapere quando deviarne dimostra una solida competenza.

Inizia a praticare!

Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.

Tag

#ruby on rails
#ruby
#colloquio
#active record
#colloquio tecnico

Condividi

Articoli correlati