Back to Course

Glossary, Senior Track Vocabulary

The technical vocabulary that comes up across the senior-track lessons. Each entry has a plain-language definition and a short code example. Existing 10 terms are alphabetical; new additions follow alphabetically among themselves.

Duck Typing

Duck typing is a style of typing where what matters is what an object can do, not what class it belongs to. The principle: "if it walks like a duck and quacks like a duck, it is a duck." Ruby uses this style heavily. Methods do not declare the types of their arguments. They call methods on whatever they receive and trust that it will respond.

class Stripe
  def process_payments(payments); puts "via Stripe"; end
end

class PayPal
  def process_payments(payments); puts "via PayPal"; end
end

def dispatch(processor, payments)
  processor.process_payments(payments) # no class check
end

dispatch(Stripe.new, []) # via Stripe
dispatch(PayPal.new, []) # via PayPal

The compiler does not enforce a contract here, so the discipline is on the developer. The test suite is what catches mistakes.

Eager Loading

Eager loading is the Active Record technique of fetching associated records up front to avoid N+1 queries. Instead of letting Rails make one extra database call per record, you tell it to load the associations in advance. The four eager-loading methods are includes, preload, eager_load, and joins. They differ in whether they generate a JOIN, fetch separately, or only filter.

# preload, always two queries, no JOIN:
Post.preload(:author).limit(50)
# SELECT * FROM posts LIMIT 50
# SELECT * FROM users WHERE id IN (...)

# eager_load, single query with LEFT OUTER JOIN:
Post.eager_load(:author).where("users.role = ?", "admin")

See Eager Loading, includes, preload, eager_load, and joins for the full decision tree.

Idempotency

An operation is idempotent when running it multiple times produces the same result as running it once. Idempotency matters anywhere code might retry: background jobs, webhooks, payment APIs, message queues. Without it, a network blip can cause a customer to be charged twice.

# Not idempotent, running twice charges twice:
def charge_user(user, cents)
  Stripe::Charge.create(amount: cents, customer: user.stripe_id)
end

# Idempotent, Stripe deduplicates by the idempotency key:
def charge_user(user, cents, request_id)
  Stripe::Charge.create(
    amount: cents,
    customer: user.stripe_id,
    idempotency_key: request_id
  )
end

Note: Idempotency is usually achieved with one of three mechanisms: a unique key on the request, a database unique constraint that rejects duplicates, or a "check before write" pattern that skips the operation if the result already exists.

Liskov Substitution

The "L" in SOLID. A subtype must be usable in place of its parent type without breaking the code that calls it. In Ruby, where there is no formal type system, the rule generalizes: any object that participates in a shared contract should honor the same expectations of what its methods do, not only what they are called.

class StripePayout
  def amount_cents; @cents; end # returns Integer
end

class PaypalPayout
  def amount_cents; "12345"; end # returns String, Liskov violation
end

# Caller assumes Integer:
total = payouts.sum(&:amount_cents) # crashes for PayPal

Both classes have an amount_cents method, but they return different types. The caller cannot substitute one for the other safely. The contract is only the method name, not the behavior, and that is not enough.

Memoization

Memoization is a caching technique. The first time a method runs, the result is stored. On subsequent calls, the stored value is returned without rerunning the calculation. In Ruby, the ||= operator is the idiomatic shortcut, scoped to an instance variable.

class Report
  def total_revenue
    @total_revenue ||= compute_total_revenue
  end

  private

  def compute_total_revenue
    # 5 seconds of database queries
    Order.completed.sum(:cents)
  end
end

report = Report.new
report.total_revenue # runs the calculation
report.total_revenue # returns the cached value

Important: Memoize only when the underlying value will not change for the lifetime of the object. Otherwise the cache returns stale data. Memoizing false or nil with ||= is a known bug source. The expression re-runs on every call when the result is falsy.

N+1 Query

An N+1 query is a performance bug where one query for a parent collection triggers one additional query per child record, leading to N+1 database round trips for N records. The bug almost always comes from accessing an association inside a loop without having preloaded it.

# N+1: one query for posts, then 50 more for authors.
posts = Post.limit(50)
posts.each { |post| puts post.author.name }
# Total: 1 + 50 = 51 queries

# Fixed with eager loading:
posts = Post.includes(:author).limit(50)
posts.each { |post| puts post.author.name }
# Total: 2 queries

N+1 is one of the most common Rails performance issues. Detect it in development with the Bullet gem.

Polymorphism

Polymorphism is the property where multiple classes respond to the same message with different behavior. Calling code depends on the message, not on the class, so swapping one implementation for another does not require changes to the caller. Polymorphism is what makes the registry pattern in Lesson 2 — The Open/Closed Principle in Real Rails Code work.

class Stripe
  def process_payments(payments); end
end

class PayPal
  def process_payments(payments); end
end

class Wise
  def process_payments(payments); end
end

[Stripe.new, PayPal.new, Wise.new].each do |processor|
  processor.process_payments(payments) # one line, three behaviors
end

Public Surface

The public surface of a class is the set of methods it exposes to callers. Other code depends on this surface. Everything else (internal helpers, instance variables, private methods) is the "private surface". It can change without affecting anyone outside the class.

class PaypalPayoutProcessor
  # Public surface, callers depend on these:
  def self.is_user_payable(user, amount); end
  def self.process_payments(payments); end

  # Private, internal helpers, free to change:
  def self.format_paypal_request(payment); end
  private_class_method :format_paypal_request
end

Senior-level class design is largely about deciding what belongs on the public surface. A small, stable public surface is what makes a class easy to refactor without breaking the rest of the codebase.

Registry (Pattern)

A registry is a small object or module whose only job is to map an identifier (often a string) to the class that handles it. Used to dispatch behavior without scattered case statements. The registry is the single place where the type-to-class mapping lives.

module PayoutProcessorType
  ALL = {
    "STRIPE" => StripePayoutProcessor,
    "PAYPAL" => PaypalPayoutProcessor
  }.freeze

  def self.get(type)
    ALL[type]
  end
end

PayoutProcessorType.get("STRIPE").process_payments(payments)

Adding a new processor is one new file plus one new line in ALL. See Lesson 2 — The Open/Closed Principle in Real Rails Code for a full walkthrough.

Shotgun Surgery

A code smell named by Martin Fowler in Refactoring. One logical change requires edits to many files. Adding a single new processor type forces you to grep for case payment.processor and update the same conditional in seven different places. The symptom is a 3-line bug fix that ends up touching 30 files.

# Adding a "Wise" processor in this codebase requires editing:
# - app/sidekiq/process_payment_worker.rb
# - app/models/user.rb
# - app/mailers/payout_mailer.rb
# - app/admin/dashboards/payouts_dashboard.rb
# - app/services/payout_csv_export.rb
# - app/views/admin/payouts/index.html.erb
# - and three more files...
#
# Each one has a copy of:
#   case payment.processor
#   when "stripe" then ...
#   when "paypal" then ...
#   end

The cure is usually to give the concept a single home (a registry, a service, a base class) and route every dispatch through it.

Adapter (Pattern)

An adapter wraps an external library or service behind an internal interface that fits the way your app wants to think. Your code talks to the adapter; the adapter talks to the outside world. Useful when an external API is shaped wrong for your domain, when you might swap providers, or when you want to test against a fake without booting the real service.

# Without an adapter, every caller knows about Stripe specifically:
Stripe::PaymentIntent.create(amount: 1000, currency: "usd")

# With an adapter:
class PaymentGateway
  def charge(amount_cents:, currency:)
    Stripe::PaymentIntent.create(amount: amount_cents, currency: currency)
  end
end

# Switching to a different provider only touches the adapter.

Cache Stampede

A failure mode where a cached value expires and many concurrent requests all miss the cache simultaneously, all decide to rebuild, and all hit the database at the same instant. The cache exists to spare the database, but the stampede makes the database take the hit anyway, in an even worse synchronized pattern. Sometimes called "thundering herd."

# Without protection, every minute, hundreds of requests
# all rebuild at the same moment:
Rails.cache.fetch("feed", expires_in: 1.minute) { expensive_query }

# With race_condition_ttl, one request rebuilds, others
# serve the slightly-stale value during the gap:
Rails.cache.fetch("feed",
  expires_in: 1.minute,
  race_condition_ttl: 10.seconds) { expensive_query }

Call Site

The location in code where a method, function, or class is invoked. Used when discussing how a piece of code is used in practice: "this method has three call sites" means three places in the codebase invoke it. Renaming a method requires updating every call site. A class with many call sites is expensive to change; one with few is cheap.

# Definition site:
def calculate_total(order)
  order.items.sum(&:price_cents)
end

# Call sites, there are two:
def show
  @total = calculate_total(@order)         # call site #1
end

def email_summary(order)
  total_cents = calculate_total(order)     # call site #2
  OrderMailer.with(total: total_cents).summary.deliver_later
end

Searching for call sites is one of the most common operations in code review: "if I change this method's signature, where does it break?" The answer is at the call sites.

Concern

A Ruby module mixed into a class to share behavior across multiple classes. Rails provides ActiveSupport::Concern, which adds conveniences for class-level macros and dependency declarations. Concerns are the Rails-idiomatic way to extract behavior that doesn't naturally fit as a separate class.

module Publishable
  extend ActiveSupport::Concern

  included do
    scope :published, -> { where.not(published_at: nil) }
  end

  def publish!
    update!(published_at: Time.current)
  end
end

class Post < ApplicationRecord
  include Publishable
end

class Article < ApplicationRecord
  include Publishable
end

Concerns work well when the shared behavior is genuinely shared. They become a smell when they accumulate unrelated capabilities or when the including class only uses some of the methods.

CSRF (Cross-Site Request Forgery)

An attack where a malicious site tricks a logged-in user's browser into submitting a form to your app, using the user's existing session. Rails defends against this with the authenticity token system: every form gets a hidden authenticity_token input, and Rails rejects POST/PATCH/DELETE requests that don't include a valid one. The defense assumes a browser-session threat model and doesn't apply to server-to-server requests like webhooks.

# Rails inserts the CSRF token automatically with form_with:
<%= form_with model: @post do |f| %>
  <!-- ... -->
<% end %>

# Renders:
<form action="/posts/42" method="post">
  <input type="hidden" name="authenticity_token" value="A7nF...">
  <!-- ... -->
</form>

# Webhook endpoints skip CSRF because they're not browser
# submissions; the webhook signature is the auth instead:
class WebhooksController < ApplicationController
  skip_before_action :verify_authenticity_token
end

Ledger Pattern

A schema pattern for tracking balances. Instead of a balance_cents column that gets updated on every transaction, you keep an append-only table of entries. Each entry is one credit or debit. The current balance is the sum of the entries. Used in money-handling apps because it preserves history, makes concurrent writes safe (INSERTs don't race the way UPDATEs do), and lets you audit how any balance got to be what it is.

# Schema:
create_table :ledger_entries do |t|
  t.references :account, null: false
  t.integer    :amount_cents, null: false   # positive credit, negative debit
  t.string     :kind, null: false           # "payment", "refund", "fee"
  t.references :source, polymorphic: true   # what caused this entry
  t.datetime   :created_at, null: false
end

# Balance:
account.ledger_entries.sum(:amount_cents)

Optimistic Locking

A concurrency-control strategy where you assume conflicts are rare and detect them at write time rather than preventing them at read time. ActiveRecord supports it via a lock_version column: each save increments the version, and a save with a stale version raises StaleObjectError. The caller catches it, reloads, retries.

# Schema:
add_column :orders, :lock_version, :integer, default: 0, null: false

# Usage:
order = Order.find(42)            # lock_version = 7
# ... meanwhile, another process saved order, lock_version is now 8
order.status = "shipped"
order.save!  # raises ActiveRecord::StaleObjectError

# Retry pattern:
begin
  order = Order.find(42)
  order.update!(status: "shipped")
rescue ActiveRecord::StaleObjectError
  retry
end

Contrast with pessimistic locking (.lock), which serializes everyone touching the row. Optimistic is cheaper when conflicts are uncommon; pessimistic is safer when they're routine.

Race Condition

A bug where the outcome depends on the relative timing of two or more operations that aren't synchronized. Classic shape in Rails: two requests both read the same value, both compute a new value from it, both write, and one write is silently lost. Race conditions usually pass tests in development (one process, one thread) and only appear under production concurrency.

# Race condition, two requests both read views_count = 10,
# both write 11. Final value is 11, not 12:
def increment_views(post)
  post.update!(views_count: post.views_count + 1)
end

# Race-free, the database does the math atomically:
def increment_views(post)
  Post.where(id: post.id).update_all("views_count = views_count + 1")
end

Reflection

In ActiveRecord, a reflection is the metadata Rails keeps about an association. When you declare belongs_to :user or has_many :posts, Rails registers a reflection on the class. Other parts of Rails read it: eager loading, form builders, fixtures, the routes for nested resources. You can query it directly with Klass.reflect_on_association(:name).

class Post < ApplicationRecord
  belongs_to :user
end

Post.reflect_on_association(:user)
# => #<ActiveRecord::Reflection::BelongsToReflection ...>
# .name        => :user
# .klass       => User
# .foreign_key => "user_id"

Post.reflections
# => { "user" => #<...> }

Service Object

A plain Ruby class that owns one business operation, exposed via a call method. Used to keep controllers thin, to make operations testable in isolation, and to coordinate side effects that don't naturally fit on any single model. Common signal that it's the right tool: the same operation is invoked from two places, or the operation has multiple side effects to coordinate.

class SignUpUser
  def initialize(params, ip:)
    @params = params
    @ip = ip
  end

  def call
    user = User.new(@params)
    return failure(user) unless user.save
    StripeCustomerJob.perform_later(user.id)
    WelcomeMailer.with(user: user).welcome_email.deliver_later
    Analytics.track("signup", user, ip: @ip)
    success(user)
  end

  # private helpers ...
end

# In the controller:
result = SignUpUser.new(user_params, ip: request.remote_ip).call

Worth noting: the 37signals school keeps logic on the model rather than extracting service objects. Service objects are a Rails idiom from the Mastodon / Forem / Gumroad school. Both are valid; pick the one your team has converged on.

State Machine

A model of behavior where an object exists in one of a finite set of states, and transitions between states are governed by explicit rules. Used in Rails for things like order statuses, payment lifecycles, subscription states, content workflows. The point is to make illegal transitions impossible: an order can't go from "canceled" back to "pending" because the state machine doesn't allow that edge.

class Order < ApplicationRecord
  enum status: { pending: 0, paid: 1, shipped: 2, canceled: 3 }

  # Legal transitions:
  STATE_TRANSITIONS = {
    pending:  [:paid, :canceled],
    paid:     [:shipped, :canceled],
    shipped:  [],
    canceled: [],
  }.freeze

  def can_transition_to?(new_state)
    STATE_TRANSITIONS[status.to_sym].include?(new_state.to_sym)
  end
end

Gems like aasm and state_machines formalize the pattern. Hand-rolled with an enum and a transition table works fine for simple cases.

Webhook

A server-to-server HTTP POST that an external service sends to your app when something happens on their side. Stripe sends webhooks for payment events. GitHub sends webhooks for pushes. Slack sends webhooks for slash commands. Unlike a polling API ("ask the service every minute if anything changed"), webhooks are push-based: the service notifies you when there's news.

# routes.rb
post "/webhooks/stripe", to: "webhooks#stripe"

# controller
class WebhooksController < ApplicationController
  skip_before_action :verify_authenticity_token

  def stripe
    event = Stripe::Webhook.construct_event(
      request.body.read,
      request.env["HTTP_STRIPE_SIGNATURE"],
      ENV.fetch("STRIPE_WEBHOOK_SECRET")
    )
    ProcessStripeEventJob.perform_later(event.id)
    head :ok
  end
end

Three properties every webhook handler needs: verify the signature, make event processing idempotent (events can arrive more than once), and stay correct under reordering (events can arrive out of order). See Payments · Webhooks for the full breakdown.

← Course Home Senior Track · Glossary · 22 terms