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-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.
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.
# 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# 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<%# 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.
# 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# 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)
endActive 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.
# 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# 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# 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, seedMigrationen 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.
# 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# 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# 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 sollteDie 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.
# 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# 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.
# 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# 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#draftsGenerierte 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.
# 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# 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
endCallbacks 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.
# 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# 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
endDas 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.
# 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# 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# 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
endConcerns 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.
# 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# 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# 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
endBest 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.
# 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# 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# 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) }
endbuild 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.
# 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# 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# 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)# 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: CleanupJobActive 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.
# 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# app/controllers/api/v1/products_controller.rb
module Api
module V1
class ProductsController < BaseController
before_action :set_product, only: [:show, :update, :destroy]
def index
@products = Product.active
.includes(:category)
.page(params[:page])
.per(params[:per_page] || 20)
render json: {
data: ProductSerializer.new(@products).serializable_hash,
meta: pagination_meta(@products)
}
end
def show
render json: ProductSerializer.new(@product, include: [:category, :reviews])
end
def create
@product = Product.new(product_params)
@product.save!
render json: ProductSerializer.new(@product), status: :created
end
def update
@product.update!(product_params)
render json: ProductSerializer.new(@product)
end
def destroy
@product.destroy!
head :no_content
end
private
def set_product
@product = Product.find(params[:id])
end
def product_params
params.require(:product).permit(:name, :description, :price, :category_id)
end
def pagination_meta(collection)
{
current_page: collection.current_page,
total_pages: collection.total_pages,
total_count: collection.total_count
}
end
end
end
end# app/serializers/product_serializer.rb
# 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
endAPI-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.
# 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# 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# 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
endFü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.
# config/environments/production.rb
config.action_controller.perform_caching = true
config.cache_store = :redis_cache_store, {
url: ENV['REDIS_URL'],
namespace: 'myapp:cache',
expires_in: 1.day,
race_condition_ttl: 10.seconds
}<%# app/views/products/index.html.erb %>
<%# Fragment Caching 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 %># 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# 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.
# 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# 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# 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# 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
endWesentliche 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.
# 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# 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)# 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] %># 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# 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:'
endRegelmäß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.
# 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# 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# 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
endPundit 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.
# app/repositories/base_repository.rb
class BaseRepository
def initialize(model_class)
@model_class = model_class
end
def all
@model_class.all
end
def find(id)
@model_class.find(id)
end
def find_by(attributes)
@model_class.find_by(attributes)
end
def create(attributes)
@model_class.create(attributes)
end
def update(record, attributes)
record.update(attributes)
end
def delete(record)
record.destroy
end
end# app/repositories/product_repository.rb
class ProductRepository < BaseRepository
def initialize
super(Product)
end
def active
@model_class.where(active: true)
end
def in_category(category_id)
@model_class.where(category_id: category_id)
end
def search(query)
@model_class.where('name ILIKE ? OR description ILIKE ?',
"%#{query}%", "%#{query}%")
end
def with_stock
@model_class.where('stock_quantity > 0')
end
def bestsellers(limit: 10)
@model_class
.joins(:order_items)
.group(:id)
.order('COUNT(order_items.id) DESC')
.limit(limit)
end
def for_homepage
active
.with_stock
.includes(:category, :primary_image)
.order(featured: :desc, created_at: :desc)
.limit(12)
end
end# 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
endRepository 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.
# app/commands/base_command.rb
class BaseCommand
include ActiveModel::Validations
def self.call(*args)
new(*args).call
end
def call
return failure(errors) unless valid?
execute
end
private
def execute
raise NotImplementedError
end
def success(data = nil)
CommandResult.success(data)
end
def failure(errors)
CommandResult.failure(errors)
end
end
CommandResult = Struct.new(:success, :data, :errors, keyword_init: true) do
def success? = success
def failure? = !success
def self.success(data)
new(success: true, data: data, errors: [])
end
def self.failure(errors)
new(success: false, data: nil, errors: Array(errors))
end
end# app/commands/orders/create_order_command.rb
module Orders
class CreateOrderCommand < BaseCommand
attr_reader :user, :items, :shipping_address
validates :user, presence: true
validates :items, presence: true
validate :validate_items_availability
def initialize(user:, items:, shipping_address:)
@user = user
@items = items
@shipping_address = shipping_address
end
private
def execute
order = nil
ActiveRecord::Base.transaction do
order = Order.create!(
user: user,
shipping_address: shipping_address,
status: 'pending'
)
items.each do |item|
order.items.create!(
product_id: item[:product_id],
quantity: item[:quantity],
unit_price: Product.find(item[:product_id]).price
)
end
order.calculate_total!
end
OrderCreatedEvent.broadcast(order)
success(order)
rescue ActiveRecord::RecordInvalid => e
failure(e.message)
end
def validate_items_availability
items.each do |item|
product = Product.find_by(id: item[:product_id])
unless product&.stock_quantity&.>= item[:quantity]
errors.add(:items, "Produkt #{item[:product_id]} nicht verfügbar")
end
end
end
end
end# app/queries/orders/user_orders_query.rb
module Orders
class UserOrdersQuery
def initialize(user, params = {})
@user = user
@params = params
end
def call
orders = @user.orders.includes(:items, items: :product)
orders = apply_status_filter(orders)
orders = apply_date_filter(orders)
orders = apply_sorting(orders)
orders.page(@params[:page]).per(@params[:per_page] || 20)
end
private
def apply_status_filter(orders)
return orders unless @params[:status]
orders.where(status: @params[:status])
end
def apply_date_filter(orders)
orders = orders.where('created_at >= ?', @params[:from]) if @params[:from]
orders = orders.where('created_at <= ?', @params[:to]) if @params[:to]
orders
end
def apply_sorting(orders)
case @params[:sort]
when 'oldest' then orders.order(created_at: :asc)
when 'total_desc' then orders.order(total: :desc)
else orders.order(created_at: :desc)
end
end
end
end# Controller 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
endCQRS 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.
# 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# 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
endimport consumer from "./consumer"
const chatChannel = consumer.subscriptions.create(
{ channel: "ChatChannel", room_id: roomId },
{
connected() {
console.log("Connected to chat")
},
disconnected() {
console.log("Disconnected from chat")
},
received(data) {
switch(data.type) {
case 'message':
this.appendMessage(data.message)
break
case 'typing':
this.showTypingIndicator(data.user)
break
case 'presence':
this.updatePresence(data)
break
}
},
sendMessage(content) {
this.perform('send_message', { content: content })
},
notifyTyping() {
this.perform('typing')
}
}
)Action Cable 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.
# 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# 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# 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# 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
endZeilenebene 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.
# 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# 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)# 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
endFü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.
# 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
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 /app /app
COPY /usr/local/bundle /usr/local/bundle
ENV RAILS_ENV=production
ENV RAILS_LOG_TO_STDOUT=true
EXPOSE 3000
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]# docker-compose.production.yml
version: '3.8'
services:
web:
build: .
environment:
- DATABASE_URL=postgres://user:pass@db/app_production
- REDIS_URL=redis://redis:6379/0
- SECRET_KEY_BASE=${SECRET_KEY_BASE}
depends_on:
- db
- redis
deploy:
replicas: 3
resources:
limits:
memory: 512M
sidekiq:
build: .
command: bundle exec sidekiq
environment:
- DATABASE_URL=postgres://user:pass@db/app_production
- REDIS_URL=redis://redis:6379/0
depends_on:
- db
- redis
deploy:
replicas: 2
db:
image: postgres:15-alpine
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD=${DB_PASSWORD}
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
volumes:
postgres_data:
redis_data: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.
# 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 } %># 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()
}
}# 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"# 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...# 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
endRails 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
Teilen
Verwandte Artikel

ActiveRecord: N+1-Abfrageprobleme in Ruby on Rails beheben
Vollständiger Leitfaden zur Erkennung und Behebung von N+1-Abfragen in Rails mit ActiveRecord. Beherrsche includes, preload, eager_load und automatisierte Erkennungstools.

Ruby on Rails 7: Hotwire und Turbo fuer reaktive Anwendungen
Vollstaendiger Leitfaden zu Hotwire und Turbo in Rails 7. Reaktive Anwendungen ohne JavaScript mit Turbo Drive, Frames und Streams entwickeln.

Solid Queue und Solid Cache in Rails 8: Umfassender Leitfaden für technische Vorstellungsgespräche 2026
Tiefgehende Analyse von Solid Queue und Solid Cache – den datenbankgestützten Standardkomponenten in Rails 8. Architektur, Konfiguration, Nebenläufigkeitssteuerung und prüfungsrelevantes Wissen für 2026.