Ruby on Rails Interview-Fragen: Top 25 in 2026

Die 25 häufigsten Ruby on Rails Interview-Fragen. MVC-Architektur, Active Record, Migrationen, RSpec-Tests, REST-APIs mit detaillierten Antworten und Codebeispielen.

Ruby on Rails Interview-Fragen - Vollständiger Leitfaden

Ruby-on-Rails-Interviews bewerten die Beherrschung des beliebtesten Ruby-Frameworks, das Verständnis der MVC-Architektur, des Active-Record-ORMs sowie die Fähigkeit, robuste Webanwendungen nach der Philosophie "Convention over Configuration" zu entwickeln. Dieser Leitfaden behandelt die 25 häufigsten Fragen, von Rails-Grundlagen bis zu fortgeschrittenen Produktionsmustern.

Tipp für das Interview

Recruiter schätzen Kandidaten, die die Rails-Philosophie verstehen: "Convention over Configuration", DRY (Don't Repeat Yourself) und die Rails-Way-Patterns. Zu erklären, warum Rails bestimmte Architekturentscheidungen trifft, macht den Unterschied.

Ruby on Rails: Grundlagen

Frage 1: Erklären Sie das MVC-Pattern in Ruby on Rails

Das Model-View-Controller-Pattern (MVC) ist der architektonische Kern von Rails. Es trennt die Verantwortlichkeiten in drei Schichten für bessere Wartbarkeit und Testbarkeit des Codes.

ruby
# app/models/article.rb
# Das Model verwaltet Daten und Geschäftslogik
class Article < ApplicationRecord
  # Datenvalidierungen
  validates :title, presence: true, length: { minimum: 5 }
  validates :body, presence: true

  # Assoziationen mit anderen Models
  belongs_to :author, class_name: 'User'
  has_many :comments, dependent: :destroy
  has_many :tags, through: :article_tags

  # Scopes für wiederverwendbare Abfragen
  scope :published, -> { where(published: true) }
  scope :recent, -> { order(created_at: :desc).limit(10) }

  # Lebenszyklus-Callbacks
  before_save :generate_slug

  private

  def generate_slug
    self.slug = title.parameterize if title_changed?
  end
end
ruby
# app/controllers/articles_controller.rb
# Der Controller empfängt Requests und orchestriert die Antwort
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: 'Artikel erfolgreich erstellt.'
    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 %>
<%# Die View zeigt die Daten im HTML-Format an %>
<article class="article-detail">
  <header>
    <h1><%= @article.title %></h1>
    <p class="meta">
      Von <%= @article.author.name %>      <%= l @article.created_at, format: :long %>
    </p>
  </header>

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

  <%# Partial für Kommentare %>
  <%= render @comments %>
</article>

Der typische Ablauf: Der Request erreicht den Router, der ihn an den passenden Controller weiterleitet. Der Controller interagiert mit dem Model, um Daten abzurufen oder zu ändern, und übergibt diese dann der View für das HTML-Rendering.

Frage 2: Was ist Active Record und wie funktioniert das ORM von Rails?

Active Record ist das ORM (Object-Relational Mapping) von Rails, das das Active-Record-Pattern implementiert. Jede Model-Klasse repräsentiert eine Datenbanktabelle, und jede Instanz repräsentiert eine Zeile.

ruby
# app/models/user.rb
# Active Record ordnet Spalten automatisch Attributen zu
class User < ApplicationRecord
  # Die Tabelle 'users' wird automatisch zugeordnet
  # Spalten: id, email, name, created_at, updated_at

  has_secure_password # BCrypt für das Passwort

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

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

  # Callbacks
  before_save :normalize_email

  # Klassenmethoden für Abfragen
  def self.admins
    joins(:roles).where(roles: { name: 'admin' })
  end

  private

  def normalize_email
    self.email = email.downcase.strip
  end
end
ruby
# Beispiele für Active-Record-Abfragen
# Rails-Konsole oder innerhalb eines Service

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

# Lesen mit Bedingungen
active_users = User.where(active: true).order(:name)
user = User.find_by(email: 'dev@example.com')

# Verkettete Abfragen (Lazy Evaluation)
recent_admins = User.admins
                    .where('created_at > ?', 1.month.ago)
                    .includes(:profile)
                    .limit(10)

# N+1-Vermeidung mit Eager Loading
articles = Article.includes(:author, :comments).published

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

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

Active Record wandelt Ruby-Methoden in optimierte SQL-Abfragen um. Methoden wie where, joins, includes sind lazy: Die Abfrage wird erst beim Iterieren oder beim Aufruf von to_a ausgeführt.

Frage 3: Erklären Sie das Migrationssystem von Rails

Migrationen ermöglichen es, das Datenbankschema mit Ruby zu versionieren. Sie sind reversibel und ermöglichen eine kontrollierte Weiterentwicklung der Datenstruktur.

ruby
# db/migrate/20260203100000_create_products.rb
# Migration zum Anlegen einer Tabelle
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 und updated_at automatisch
    end

    # Indizes für die Performance
    add_index :products, :name
    add_index :products, [:category_id, :active]
  end
end
ruby
# db/migrate/20260203110000_add_slug_to_products.rb
# Migration zum Ändern einer existierenden Tabelle
class AddSlugToProducts < ActiveRecord::Migration[7.1]
  def change
    add_column :products, :slug, :string
    add_index :products, :slug, unique: true

    # Bestehende Slugs befüllen
    reversible do |dir|
      dir.up do
        Product.find_each do |product|
          product.update_column(:slug, product.name.parameterize)
        end
      end
    end

    # Nach Befüllung NOT NULL setzen
    change_column_null :products, :slug, false
  end
end
bash
# Wesentliche Migrationsbefehle
rails db:migrate              # Ausstehende Migrationen ausführen
rails db:rollback             # Letzte Migration rückgängig machen
rails db:rollback STEP=3      # Letzte 3 Migrationen rückgängig machen
rails db:migrate:status       # Status der Migrationen anzeigen
rails db:seed                 # db/seeds.rb ausführen
rails db:reset                # Drop, create, migrate, seed

Migrationen müssen reversibel sein. Die Methode change ist intelligent und kann gängige Operationen automatisch umkehren. Für komplexe Fälle up und down separat verwenden.

Fortgeschrittenes Active Record

Frage 4: Wie optimiert man N+1-Abfragen in Rails?

Das N+1-Problem tritt auf, wenn auf eine initiale Abfrage N zusätzliche Abfragen folgen, um Assoziationen zu laden. Rails bietet mehrere Eager-Loading-Methoden zur Lösung dieses Problems.

ruby
# app/controllers/orders_controller.rb
class OrdersController < ApplicationController
  def index
    # ❌ N+1-PROBLEM: 1 Abfrage + N Abfragen pro Bestellung
    # @orders = Order.all
    # In der View: order.user.name erzeugt eine Abfrage pro Bestellung

    # ✅ LÖSUNG mit includes (Eager Loading)
    @orders = Order.includes(:user, :items)
                   .where(status: 'completed')
                   .order(created_at: :desc)
    # Erzeugt insgesamt nur 3 Abfragen
  end

  def show
    # includes: lädt Assoziationen separat (2-3 Abfragen)
    @order = Order.includes(items: :product).find(params[:id])

    # preload: erzwingt separates Laden
    @order = Order.preload(:items, :user).find(params[:id])

    # eager_load: erzwingt LEFT OUTER JOIN (1 Abfrage)
    @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 mit standardmäßigem includes
  scope :with_details, -> { includes(:user, items: :product) }

  # Counter Cache zur Vermeidung von COUNT-Abfragen
  # Erfordert: add_column :users, :orders_count, :integer, default: 0
  belongs_to :user, counter_cache: true
end
ruby
# N+1-Erkennung mit der Bullet-Gem (Entwicklung)
# config/environments/development.rb
config.after_initialize do
  Bullet.enable = true
  Bullet.alert = true
  Bullet.bullet_logger = true
  Bullet.rails_logger = true
end

# Bullet zeigt Warnungen, wenn:
# - Eine N+1-Abfrage erkannt wird
# - Unnötiges Eager Loading vorliegt
# - Ein Counter Cache verwendet werden sollte

Die Regel: standardmäßig includes verwenden (Rails wählt die optimale Strategie), preload wenn separate Abfragen erzwungen werden sollen, eager_load wenn auf Assoziationen gefiltert wird.

Frage 5: Erklären Sie Scopes und Query Objects in Rails

Scopes kapseln wiederverwendbare Abfragebedingungen. Für komplexe Abfragen bieten Query Objects bessere Organisation und Testbarkeit.

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

  # Scopes mit Parametern
  scope :cheaper_than, ->(price) { where('price < ?', price) }
  scope :in_category, ->(category) { where(category: category) }

  # Verkettbare Scopes
  scope :available, -> { active.in_stock }

  # Scope mit Joins
  scope :with_recent_orders, -> {
    joins(:order_items)
      .where('order_items.created_at > ?', 30.days.ago)
      .distinct
  }

  # Scope mit 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 für komplexe Suchen
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

# Verwendung im Controller
@products = ProductsSearchQuery.new(Product.active).call(params)

Scopes eignen sich perfekt für einfache, wiederverwendbare Bedingungen. Query Objects passen zu komplexen Suchen mit mehreren optionalen Filtern und Kompositionslogik.

Bereit für deine Ruby on Rails-Interviews?

Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.

Routing und Controllers

Frage 6: Wie funktioniert RESTful Routing in Rails?

Rails fördert RESTful-Routen, die HTTP-Verben auf CRUD-Aktionen abbilden. Der Router übersetzt URLs in spezifische Controller-Aufrufe.

ruby
# config/routes.rb
Rails.application.routes.draw do
  # Standardmäßige RESTful-Routen (7 Aktionen)
  resources :articles do
    # Verschachtelte Routen
    resources :comments, only: [:create, :destroy]

    # Member-Routen (wirken auf eine Instanz)
    member do
      post :publish
      delete :archive
    end

    # Collection-Routen (wirken auf die Sammlung)
    collection do
      get :drafts
      get :search
    end
  end

  # API-Routen mit Namespace
  namespace :api do
    namespace :v1 do
      resources :products, only: [:index, :show, :create, :update] do
        resources :reviews, shallow: true
      end
    end
  end

  # Benutzerdefinierte Route
  get 'dashboard', to: 'dashboard#index'

  # Routenbeschränkungen
  constraints(SubdomainConstraint.new) do
    resources :admin_settings
  end

  # Root-Route
  root 'home#index'
end
bash
# rails routes - Zeigt alle generierten Routen
#
# 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

Generierte Route-Helper (article_path(@article), new_article_path) erlauben es, URLs dynamisch und wartbar zu referenzieren.

Frage 7: Erklären Sie Callbacks und Filter in Controllern

Callbacks (before_action, after_action, around_action) erlauben es, Code vor, nach oder rund um Controller-Aktionen auszuführen.

ruby
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  # CSRF-Schutz standardmäßig aktiviert
  protect_from_forgery with: :exception

  # Globaler Callback für die Authentifizierung
  before_action :authenticate_user!

  # Globale Fehlerbehandlung
  rescue_from ActiveRecord::RecordNotFound, with: :not_found
  rescue_from ActionController::ParameterMissing, with: :bad_request

  private

  def not_found
    render json: { error: 'Ressource nicht gefunden' }, 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 mit Optionen
  before_action :require_admin
  before_action :set_product, only: [:show, :edit, :update, :destroy]
  after_action :log_activity, only: [:create, :update, :destroy]

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

  def create
    @product = Product.new(product_params)

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

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

Callbacks werden in der Reihenfolge ihrer Deklaration ausgeführt. skip_before_action in Subklassen verwenden, um geerbte Callbacks zu deaktivieren. Callbacks mit zu viel Geschäftslogik vermeiden: Service Objects bevorzugen.

Services und Architektur

Frage 8: Wie implementiert man Service Objects in Rails?

Service Objects kapseln komplexe Geschäftslogik, die weder zu Models noch zu Controllern gehört. Sie verbessern die Testbarkeit und folgen dem Single-Responsibility-Prinzip.

ruby
# app/services/order_processor.rb
# Service Object mit standardisierter Schnittstelle
class OrderProcessor
  def initialize(order, payment_method:)
    @order = order
    @payment_method = payment_method
  end

  def call
    return failure('Bestellung bereits verarbeitet') 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("Zahlung fehlgeschlagen: #{e.message}")
  rescue InsufficientStockError => e
    failure("Lagerbestand unzureichend: #{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: "Bestellung ##{@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: 'Bestellung bestätigt!'
      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

Das Service-Object-Pattern folgt einer einfachen Konvention: eine Klasse, eine Verantwortung, eine öffentliche call-Methode. Ein Result-Objekt zurückzugeben ermöglicht eine saubere Behandlung von Erfolg und Fehler.

Frage 9: Erklären Sie Concerns in Rails

Concerns ermöglichen es, Code zwischen Models oder Controllern zu extrahieren und zu teilen. Sie nutzen ActiveSupport::Concern für eine saubere Inklusionssyntax.

ruby
# app/models/concerns/sluggable.rb
# Wiederverwendbares Concern zur Slug-Generierung
module Sluggable
  extend ActiveSupport::Concern

  included do
    # Beim Einbinden ausgeführter Code
    before_validation :generate_slug, if: :should_generate_slug?
    validates :slug, presence: true, uniqueness: true
  end

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

  # Instanzmethoden
  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 # Optional, :title als Standard
end

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

  sluggable_source :name
end
ruby
# app/controllers/concerns/pagination.rb
# Concern für 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

Concerns sind nützlich für wirklich gemeinsam genutzten Code. Vermeiden, Concerns nur zu erstellen, um ein Model zu "verkürzen": Das versteckt Komplexität, ohne sie zu reduzieren.

Testen mit RSpec

Frage 10: Wie strukturiert man RSpec-Tests in Rails?

RSpec ist das Standard-Testing-Framework für Rails. Eine gute Teststruktur umfasst Model Specs, Controller Specs, Service Specs und Integrationstests.

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

RSpec.describe User, type: :model do
  # Factories mit 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 'validiert das E-Mail-Format' 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 'gibt Vor- und Nachname kombiniert zurück' do
      user = build(:user, first_name: 'John', last_name: 'Doe')
      expect(user.full_name).to eq('John Doe')
    end

    it 'behandelt fehlenden Nachnamen' do
      user = build(:user, first_name: 'John', last_name: nil)
      expect(user.full_name).to eq('John')
    end
  end

  describe '.active' do
    it 'gibt nur aktive Benutzer zurück' 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 'wenn die Bestellung gültig ist' do
      before do
        allow(PaymentGateway).to receive(:charge).and_return(
          OpenStruct.new(success?: true, transaction_id: 'txn_123')
        )
      end

      it 'verarbeitet die Bestellung erfolgreich' do
        result = subject.call

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

      it 'verringert den Produktbestand' do
        expect { subject.call }.to change { product.reload.stock_quantity }.by(-2)
      end

      it 'sendet die Bestätigungs-E-Mail' do
        expect { subject.call }
          .to have_enqueued_mail(OrderMailer, :confirmation)
          .with(order)
      end
    end

    context 'wenn die Zahlung fehlschlägt' do
      before do
        allow(PaymentGateway).to receive(:charge).and_return(
          OpenStruct.new(success?: false, error: 'Card declined')
        )
      end

      it 'gibt ein Fehlerergebnis zurück' do
        result = subject.call

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

      it 'aktualisiert den Bestellstatus nicht' 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 'gibt die Produktliste zurück' do
      get '/api/v1/products', headers: headers

      expect(response).to have_http_status(:ok)
      expect(json_response['data'].size).to eq(3)
    end

    it 'filtert nach Kategorie' 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: 'Neues Produkt', price: 99.99, category_id: create(:category).id } }
    end

    it 'erstellt ein neues Produkt' 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

Best Practices: let für Daten, describe für Methoden/Kontexte, context für Bedingungen und it für spezifische Assertions. Ein Test sollte eine Sache testen.

Frage 11: Wie verwendet man Factories mit FactoryBot?

FactoryBot ermöglicht es, Testdaten deklarativ und wartbar zu erstellen. Factories ersetzen statische Fixtures.

ruby
# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    # Sequenzen für Eindeutigkeit
    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 für Variationen
    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

    # Geerbte Factory
    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
# Verwendung in Tests
RSpec.describe OrderProcessor do
  # build: nicht persistierte Instanz
  let(:user) { build(:user) }

  # create: in DB persistiert
  let(:order) { create(:order, :with_items, user: user) }

  # create_list: mehrere Instanzen
  let(:products) { create_list(:product, 5) }

  # Traits kombinieren
  let(:admin) { create(:user, :admin, :with_profile) }

  # Attribute überschreiben
  let(:expensive_order) { create(:order, :with_items, items_count: 10) }

  # build_stubbed: schneller, für Unit-Tests
  let(:stubbed_user) { build_stubbed(:user) }
end

build oder build_stubbed gegenüber create bevorzugen, wenn keine Persistenz nötig ist: Das beschleunigt die Tests deutlich.

Background Jobs

Frage 12: Wie verwendet man Active Job und Sidekiq in Rails?

Active Job bietet eine einheitliche Schnittstelle für Hintergrundjobs, unabhängig vom Backend (Sidekiq, Resque usw.). Sidekiq ist die populäre Wahl wegen seiner Performance mit Redis.

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

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

  # Sidekiq-Optionen (bei Sidekiq-Backend)
  sidekiq_options retry: 5, backtrace: true

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

    OrderProcessor.new(order).call
  rescue ActiveRecord::RecordNotFound
    # Bestellung zwischen Einreihen und Ausführung gelöscht
    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

  # Rate Limiting mit Sidekiq Enterprise oder Throttle-Gem
  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
# Jobs einreihen
# Sofort
ProcessOrderJob.perform_later(order.id)

# Verzögert
ProcessOrderJob.set(wait: 5.minutes).perform_later(order.id)

# Zu einem bestimmten Zeitpunkt
ProcessOrderJob.set(wait_until: Date.tomorrow.noon).perform_later(order.id)

# Bestimmte Queue
ProcessOrderJob.set(queue: :critical).perform_later(order.id)

# Synchron (für Tests oder Debugging)
ProcessOrderJob.perform_now(order.id)
ruby
# config/sidekiq.yml
:concurrency: 10
:queues:
  - [critical, 3]    # Hohe Priorität, Gewicht 3
  - [default, 2]     # Mittlere Priorität, Gewicht 2
  - [mailers, 1]     # Niedrige Priorität, Gewicht 1
  - [low, 1]

:schedule:
  cleanup_job:
    cron: '0 3 * * *'  # Jeden Tag um 3 Uhr
    class: CleanupJob

Active Job abstrahiert das Backend, aber der Zugriff auf spezifische Funktionen (Batches, Rate Limiting) erfordert oft eine Kopplung an das gewählte Backend.

Bereit für deine Ruby on Rails-Interviews?

Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.

API-Entwicklung

Frage 13: Wie baut man eine RESTful API mit Rails?

Rails erleichtert den Bau von JSON-APIs mit API-only-Controllern und Serializern. Eine gute API ist versioniert, dokumentiert und sicher.

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: 'Ressource nicht gefunden', details: exception.message },
               status: :not_found
      end

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

      def bad_request(exception)
        render json: { error: 'Ungültige Anfrage', 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
# Mit der jsonapi-serializer-Gem
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

API-Best-Practices: über Namespace versionieren, passende HTTP-Codes verwenden, Sammlungen paginieren und klare Fehlermeldungen bereitstellen.

Frage 14: Wie implementiert man JWT-Authentifizierung in Rails?

JWT (JSON Web Tokens) ist eine populäre stateless Authentifizierungsmethode für APIs. Das Token kodiert die Identität und Gültigkeit des Benutzers.

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 abgelaufen'
    rescue JWT::DecodeError
      raise AuthenticationError, 'Ungültiges Token'
    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: 'Ungültige Zugangsdaten' }, 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 fehlt' 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: 'Benutzer nicht gefunden' }, status: :unauthorized
  end

  def current_user
    @current_user
  end
end

Für die Produktion in Betracht ziehen: Refresh Tokens, Token-Blacklisting beim Logout und kurze Ablaufzeiten. Gems wie devise-jwt vereinfachen die Implementierung.

Caching und Performance

Frage 15: Wie implementiert man Caching in Rails?

Rails bietet mehrere Caching-Ebenen: Fragment Caching, Russian Doll Caching, Low-Level-Caching. Die Wahl hängt vom Anwendungsfall ab.

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 mit automatischem Cache-Schlüssel %>
<% @products.each do |product| %>
  <%# Cache basiert auf updated_at des Produkts %>
  <% cache product do %>
    <%= render product %>
  <% end %>
<% end %>

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

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

<%# Bedingtes Caching %>
<% cache_if current_user.nil?, @product do %>
  <%= render @product %>
<% end %>
ruby
# app/models/product.rb
class Product < ApplicationRecord
  # Touch des Parents zum Invalidieren des Russian-Doll-Caches
  belongs_to :category, touch: true

  # Benutzerdefinierter Cache-Schlüssel
  def cache_key_with_version
    "#{super}/#{reviews.maximum(:updated_at)&.to_i}"
  end
end
ruby
# Low-Level-Caching in Services
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 mit Schutz vor Race Conditions
Rails.cache.fetch('popular_products', expires_in: 1.hour, race_condition_ttl: 10.seconds) do
  Product.bestsellers.limit(10).to_a
end

# Explizite Invalidierung
Rails.cache.delete('dashboard:stats')
Rails.cache.delete_matched('products:*')

Russian Doll Caching ist effektiv, weil nur geänderte Fragmente neu generiert werden. touch: true auf Assoziationen verwenden, um die Invalidierung zu propagieren.

Frage 16: Wie optimiert man die Performance einer Rails-Anwendung?

Die Rails-Optimierung umfasst mehrere Aspekte: DB-Abfragen, Caching, Assets und Architektur. Ein methodisches Vorgehen mit Monitoring ist essenziell.

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

# app/models/order.rb
class Order < ApplicationRecord
  # Zusammengesetzte Indizes für häufige Abfragen
  # add_index :orders, [:user_id, :status, :created_at]

  # Nur die benötigten Spalten auswählen
  scope :summary, -> { select(:id, :status, :total, :created_at) }

  # Stapelverarbeitung für große Datenmengen
  def self.process_pending
    pending.find_each(batch_size: 1000) do |order|
      ProcessOrderJob.perform_later(order.id)
    end
  end

  # Wiederkehrende Berechnungen vermeiden
  def self.revenue_by_month
    completed
      .group("DATE_TRUNC('month', created_at)")
      .sum(:total)
  end
end
ruby
# Speicheroptimierung
# 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 mit 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 und Pagination
class ProductsController < ApplicationController
  def index
    @products = Product.active
                       .includes(:category, :primary_image)
                       .page(params[:page])
                       .per(24)

    # Prefetch für die nächste Seite
    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

Wesentliche Tools: rack-mini-profiler für Profiling, bullet für N+1-Erkennung, New Relic oder Scout für Produktions-Monitoring.

Sicherheit

Frage 17: Was sind die Best Practices für Sicherheit in Rails?

Rails enthält Standardschutzmechanismen gegen gängige Schwachstellen. Diese Schutzmechanismen zu verstehen und korrekt zu konfigurieren ist entscheidend.

ruby
# CSRF-Schutz
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  # Standardmäßig aktiviert, löst Exception aus, wenn das Token ungültig ist
  protect_from_forgery with: :exception

  # Für APIs :null_session verwenden
  # protect_from_forgery with: :null_session
end

# In Views ist das Token in Formularen automatisch enthalten
# <%= form_with ... %> beinhaltet authenticity_token

# Für AJAX-Anfragen
# Header X-CSRF-Token mit dem Wert von csrf_meta_tags hinzufügen
ruby
# Schutz vor SQL-Injection
# ✅ Interpolierte Parameter werden automatisch escaped
User.where('email = ?', params[:email])
User.where(email: params[:email])

# ❌ GEFAHR - Direkte Interpolation
User.where("email = '#{params[:email]}'")

# ✅ Für dynamische ORDER-Klauseln
ALLOWED_SORTS = %w[name created_at price].freeze
sort_column = ALLOWED_SORTS.include?(params[:sort]) ? params[:sort] : 'name'
Product.order(sort_column)
ruby
# XSS-Schutz
# Rails escaped HTML in Views automatisch

# ✅ Automatisch escaped
<%= user.name %>

# ❌ Gefährlich - nicht escapeter Inhalt
<%== user.bio %>
<%= raw user.bio %>
<%= user.bio.html_safe %>

# ✅ Für sicheres HTML sanitize verwenden
<%= 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
    # Explizite Whitelist erlaubter Attribute
    params.require(:user).permit(:name, :email, :avatar)

    # Nur für Administratoren
    if current_user.admin?
      params.require(:user).permit(:name, :email, :role, :active)
    else
      params.require(:user).permit(:name, :email)
    end
  end
end
ruby
# Sicherheits-Header
# 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

Regelmäßig mit brakeman (statische Sicherheitsanalyse) auditieren und Gems mit bundle audit aktuell halten.

Frage 18: Wie behandelt man Authentifizierung und Autorisierung in Rails?

Authentifizierung verifiziert die Identität, Autorisierung steuert die Berechtigungen. Devise verwaltet die Auth, Pundit oder CanCanCan verwalten die Autorisierung.

ruby
# Devise-Setup
# 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
# Pundit-Policies
# 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 für Sammlungen
  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 mit 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: 'Artikel aktualisiert.'
    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: 'Artikel veröffentlicht.'
  end

  rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized

  private

  def user_not_authorized
    flash[:alert] = "Sie sind nicht berechtigt, diese Aktion auszuführen."
    redirect_back(fallback_location: root_path)
  end
end

Pundit ist expliziter und besser testbar als CanCanCan. Jede Aktion hat eine entsprechende Policy-Methode, und Scopes filtern Sammlungen automatisch.

Fortgeschrittenes Rails

Frage 19: Erklären Sie das Repository-Pattern in Rails

Das Repository-Pattern isoliert die Datenzugriffslogik vom Rest der Anwendung. Auch wenn Rails Active Record (ein anderes Pattern) verwendet, kann Repository für komplexe Fälle nützlich sein.

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
# Verwendung in einem 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

# Erleichtert das Testen mit Mocks
RSpec.describe ProductSearchService do
  let(:repository) { instance_double(ProductRepository) }
  let(:service) { described_class.new(repository: repository) }

  it 'filtert nach Kategorie' 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 ist in Rails optional, da Active Record bereits ein hervorragendes Pattern ist. Für komplexe Abfragen verwenden oder wenn Storage-Isolation wichtig ist.

Frage 20: Wie implementiert man das CQRS-Pattern in Rails?

CQRS (Command Query Responsibility Segregation) trennt Lese- und Schreiboperationen. In Rails übersetzt sich das in getrennte Klassen für Queries und 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, "Produkt #{item[:product_id]} nicht verfügbar")
        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 mit 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: 'Bestellung erstellt!'
    else
      flash.now[:alert] = result.errors.join(', ')
      render :new, status: :unprocessable_entity
    end
  end
end

CQRS glänzt bei komplexen Anwendungen mit asymmetrischen Lese-/Schreibanforderungen. Für einfaches CRUD ist es Over-Engineering.

Frage 21: Wie behandelt man WebSockets mit Action Cable?

Action Cable integriert WebSockets in Rails für bidirektionale Echtzeitkommunikation. Es nutzt Redis zur Synchronisation zwischen Servern.

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
      # Über Session-Cookie
      if verified_user = User.find_by(id: cookies.encrypted[:user_id])
        verified_user
      # Über JWT für 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])

    # Berechtigungen prüfen
    unless @room.accessible_by?(current_user)
      reject
      return
    end

    stream_for @room

    # Andere über die Anwesenheit informieren
    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']
    )

    # An alle Abonnenten senden
    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 behandelt Reconnects und Synchronisation automatisch. In der Produktion Redis als Adapter konfigurieren und entsprechend der gleichzeitigen Verbindungen skalieren.

Frage 22: Wie implementiert man Multi-Tenancy in Rails?

Multi-Tenancy ermöglicht es einer Anwendung, mehrere isolierte Kunden (Tenants) zu bedienen. Drei Hauptansätze: auf Datenbankebene, auf Schemaebene oder auf Zeilenebene.

ruby
# Multi-Tenancy auf Zeilenebene mit ActsAsTenant oder manuell
# app/models/concerns/tenant_scoped.rb
module TenantScoped
  extend ActiveSupport::Concern

  included do
    belongs_to :tenant

    # Standard-Scope auf den aktuellen Tenant
    default_scope -> { where(tenant: Current.tenant) if Current.tenant }

    # Tenant-Validierung
    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
    # Über Subdomain
    if request.subdomain.present? && request.subdomain != 'www'
      Tenant.find_by!(subdomain: request.subdomain)
    # Über Header (für APIs)
    elsif request.headers['X-Tenant-ID'].present?
      Tenant.find(request.headers['X-Tenant-ID'])
    # Über Benutzer
    elsif current_user
      current_user.tenant
    end
  rescue ActiveRecord::RecordNotFound
    redirect_to root_url(subdomain: 'www'), alert: 'Tenant nicht gefunden'
  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

  # Administratoren können mehreren Tenants angehören
  has_many :tenant_memberships
  has_many :accessible_tenants, through: :tenant_memberships, source: :tenant
end
ruby
# Auf Schemaebene mit der Apartment-Gem (PostgreSQL)
# config/initializers/apartment.rb
Apartment.configure do |config|
  config.excluded_models = %w[Tenant User]
  config.tenant_names = -> { Tenant.pluck(:subdomain) }
end

# Verwendung
Apartment::Tenant.switch('acme') do
  # Alle Abfragen in diesem Block nutzen das 'acme'-Schema
  Project.all # SELECT * FROM acme.projects
end

Zeilenebene ist am einfachsten, erfordert aber ständige Aufmerksamkeit für Lecks. Schemaebene bietet bessere Isolation, kompliziert aber Migrationen. Je nach Sicherheits- und Skalierbarkeitsanforderungen wählen.

Frage 23: Wie richtet man eine Microservices-Architektur mit Rails ein?

Rails kann als Basis für eine Microservices-Architektur mit Kommunikation über HTTP/gRPC oder Message Queues dienen. Der Schlüssel liegt in einer guten Definition der Grenzen.

ruby
# HTTP-Service-Client
# 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('Service nicht verfügbar', code: response.code)
    end
  rescue Net::OpenTimeout, Net::ReadTimeout
    ServiceResult.failure('Service-Timeout')
  end
end
ruby
# Event-getriebene Kommunikation mit 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
# API-Gateway-Pattern
# app/controllers/api/v1/gateway_controller.rb
module Api
  module V1
    class GatewayController < BaseController
      # Mehrere Services aggregieren
      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} nicht verfügbar", message: e.message }
      end
    end
  end
end

Für Rails-Microservices: klare API-Verträge definieren (OpenAPI), Circuit Breaker implementieren (Gem circuitbox) und Distributed Tracing nutzen (Gem opentelemetry).

Frage 24: Wie deployt man eine Rails-Anwendung in Produktion?

Modernes Rails-Deployment nutzt Container oder PaaS. Eine robuste Produktionskonfiguration deckt Assets, Datenbank und Monitoring ab.

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

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

  # SSL erzwingen
  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

# Produktions-Image
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:

Produktions-Checkliste: SSL verpflichtend, Secrets über ENV, Health Checks, automatisierte DB-Backups, Monitoring (APM + Logs + Metriken) und konfiguriertes Alerting.

Frage 25: Was sind die neuen Features in Rails 7+, die man kennen sollte?

Rails 7+ bringt bedeutende Änderungen: Hotwire standardmäßig, Import Maps, verbesserte verschlüsselte Credentials und viele Optimierungen.

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

# Turbo Streams für Echtzeit-Updates
# 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
# Stimulus-Controller
# 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 (ohne JavaScript-Bundler)
# 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 vom 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  # Erlaubt Suchen
  encrypts :phone_number                 # Standardmäßig nicht-deterministisch
  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
# Verbesserungen an der Query-Schnittstelle
# Rails 7.1+

# Asynchrone Queries
users = User.where(active: true).load_async
# Weiterarbeiten, während die Query läuft
# Auf Ergebnisse mit users.to_a zugreifen

# 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')

# Automatische inverse_of-Erkennung
class Author < ApplicationRecord
  has_many :books # inverse_of automatisch erkannt
end

# Strict Loading standardmäßig (vermeidet N+1)
class ApplicationRecord < ActiveRecord::Base
  self.strict_loading_by_default = true
end

Rails 7+ bevorzugt Einfachheit (kein Webpack standardmäßig) und HTML-over-the-wire mit Hotwire. Dieser Ansatz reduziert die JavaScript-Komplexität und bietet gleichzeitig eine moderne Benutzererfahrung.

Fazit

Ruby-on-Rails-Interviews bewerten die Beherrschung des gesamten Frameworks und das Verständnis seiner Konventionen. Wichtige Punkte zum Merken:

Grundlagen: MVC, Active Record, Migrationen, Validierungen und Assoziationen

Architektur: Service Objects, Concerns, Query Objects und CQRS-Patterns

Performance: N+1-Abfragen, Caching (Fragment, Russian Doll, Low-Level), Eager Loading

Testing: RSpec, FactoryBot, Request Specs und Best Practices beim Testen

Sicherheit: CSRF, SQL-Injection, XSS, Strong Parameters und Authentifizierung/Autorisierung

APIs: RESTful-Design, JWT, Serializer und Versionierung

Produktion: Background Jobs, WebSockets, Deployment und Monitoring

Die Rails-Philosophie (Convention over Configuration, DRY und Rails Way) leitet alle architektonischen Entscheidungen. Diese Prinzipien zu beherrschen und zu wissen, wann von ihnen abgewichen werden sollte, demonstriert solide Expertise.

Fang an zu üben!

Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.

Tags

#ruby on rails
#ruby
#interview
#active record
#technisches interview

Teilen

Verwandte Artikel