CrackedRuby CrackedRuby

YAGNI (You Aren't Gonna Need It)

Overview

YAGNI (You Aren't Gonna Need It) represents a core principle in agile software development that advises against implementing features, abstractions, or infrastructure based on speculative future requirements. The principle originates from Extreme Programming (XP) practices and addresses the common tendency among developers to build for anticipated needs that may never materialize.

The cost of speculative development extends beyond initial implementation time. Each unnecessary feature or abstraction introduces additional code to maintain, test, document, and understand. This technical overhead accumulates over a project's lifetime, slowing future development and increasing the cognitive load for all team members. Code written today for hypothetical tomorrow requirements often becomes obsolete before the anticipated need arrives, as requirements evolve and initial assumptions prove incorrect.

YAGNI intersects with other development principles but maintains distinct focus. While DRY (Don't Repeat Yourself) addresses code duplication and KISS (Keep It Simple, Stupid) promotes simplicity in design, YAGNI specifically targets premature optimization and speculative generalization. A developer might create an overly abstract, non-repetitive, simple implementation that still violates YAGNI by solving problems that don't exist.

The principle applies across multiple dimensions of software development: feature implementation, architectural decisions, abstraction layers, configuration systems, and infrastructure setup. In each context, YAGNI recommends deferring complexity until concrete requirements emerge, allowing designs to evolve based on actual usage patterns rather than predicted scenarios.

# Violates YAGNI - complex configuration system for a single use case
class EmailService
  def initialize(config = {})
    @provider = config[:provider] || :sendgrid
    @retry_strategy = config[:retry_strategy] || :exponential
    @max_retries = config[:max_retries] || 3
    @fallback_providers = config[:fallback_providers] || []
    @rate_limiter = config[:rate_limiter]
  end
  
  def send_email(to:, subject:, body:)
    # Complex implementation handling all configured scenarios
  end
end

# Follows YAGNI - implements current requirement only
class EmailService
  def send_email(to:, subject:, body:)
    # Direct implementation using single provider
    # Can add configuration when actually needed
  end
end

The principle acknowledges that predicting future requirements remains inherently unreliable. Requirements change as products evolve, markets shift, and user feedback arrives. Building for predicted futures often results in wasted effort on features that never materialize or designs that don't fit actual future needs when they arrive.

Key Principles

The fundamental premise of YAGNI states that implementing functionality before it becomes necessary creates more problems than it solves. This occurs because speculative code introduces immediate costs without providing immediate value, and the anticipated future benefit often fails to materialize or arrives in a different form than predicted.

Present Cost vs Future Value: Every line of code written today incurs immediate costs in development time, testing effort, maintenance burden, and increased system complexity. Speculative features bet that future value will justify these present costs. However, this bet frequently loses because requirements change, features go unused, or the implementation requires substantial modification when the need actually arrives. YAGNI recognizes that deferring implementation defers these costs, allowing resources to focus on current, verified requirements.

Optimal Timing: The optimal time to implement functionality occurs when three conditions align: the requirement exists, the requirement is understood, and the implementation is needed soon. Implementing before these conditions align means working with incomplete information, increasing the likelihood of building the wrong thing or building the right thing wrong. Deferring until these conditions align allows designs to emerge from concrete constraints rather than abstract speculation.

Refactoring Over Prediction: YAGNI assumes that refactoring existing code to accommodate new requirements costs less than maintaining speculative code that may never be used. This assumption holds when refactoring skills are developed, test coverage supports safe changes, and designs remain simple enough to modify. The principle advocates investing in these enabling practices rather than attempting to predict and pre-solve future problems.

Incremental Complexity: Systems should evolve from simple implementations toward complex ones as requirements demand, rather than starting with complex designs that handle anticipated scenarios. A simple implementation serving current needs provides a concrete foundation. When new requirements arrive, the implementation can be refactored to accommodate them, guided by the actual requirement rather than a prediction. This incremental approach keeps systems as simple as possible at each stage while remaining responsive to change.

Decision Reversibility: YAGNI particularly applies to decisions that remain reversible through refactoring. If a decision cannot be easily reversed later, more upfront consideration may be warranted. However, most code-level decisions maintain reversibility given adequate tests and refactoring discipline. Architectural decisions with high reversal costs deserve more forward-thinking analysis, though even these benefit from favoring simpler approaches until complexity proves necessary.

Real vs Imagined Requirements: A requirement qualifies as real when it comes from actual users, stakeholders, or system constraints with specific use cases and acceptance criteria. Imagined requirements arise from developer predictions, hypothetical scenarios, or vague possibilities. YAGNI distinguishes between these categories and recommends implementing only against real requirements. The practice of distinguishing real from imagined requirements prevents scope creep and focuses development effort on verified needs.

# Imagined requirement - multiple authentication strategies
class AuthenticationService
  def authenticate(credentials, strategy: :database)
    case strategy
    when :database then authenticate_database(credentials)
    when :ldap then authenticate_ldap(credentials)
    when :oauth then authenticate_oauth(credentials)
    when :saml then authenticate_saml(credentials)
    end
  end
end

# Real requirement - single authentication method currently needed
class AuthenticationService
  def authenticate(email:, password:)
    user = User.find_by(email: email)
    return nil unless user
    user.authenticate(password) ? user : nil
  end
end

Test Coverage as Safety Net: YAGNI relies on comprehensive test coverage to enable safe refactoring when requirements change. Without tests, the cost of modifying existing code increases substantially, making speculative development appear more attractive as insurance against future change. However, maintaining speculative code also carries costs, and tests must cover that speculative code as well. Strong test coverage reduces the risk of change regardless of when features are implemented, allowing YAGNI to function effectively.

Domain Understanding: The principle requires sufficient domain understanding to distinguish between premature optimization and legitimate architectural foundation. Some upfront investment in architecture, infrastructure, or abstractions proves necessary even under YAGNI. The distinction lies in whether the complexity serves current requirements or anticipated future ones. Domain knowledge helps identify which architectural elements serve present needs even if their full value appears in the future.

Design Considerations

Applying YAGNI requires judgment about when to defer implementation and when current requirements justify complexity. Several factors influence this decision, each providing context for evaluating whether additional functionality or abstraction belongs in the current iteration.

Requirement Certainty: The certainty of a future requirement significantly impacts YAGNI decisions. Requirements explicitly stated in current project scope, confirmed by stakeholders, and scheduled for near-term implementation occupy a different category than hypothetical features that might someday be needed. YAGNI applies most strongly to the latter category. For requirements scheduled in the next sprint or iteration, some preparation may be reasonable. For requirements that remain speculative possibilities, YAGNI recommends deferring until they become concrete.

Cost of Later Addition: Some architectural decisions carry high costs if deferred. Database schema changes in production systems, public API contracts, or serialization formats might justify more upfront consideration even under YAGNI. The principle does not advocate ignoring all future implications, but rather recommends evaluating whether the cost of later change genuinely exceeds the cost of present complexity. In many cases, developers overestimate the cost of later change, particularly when adequate tests and refactoring skills exist.

Change Frequency: Components that change frequently benefit more strongly from YAGNI than stable components. Business logic that evolves with requirements should remain simple and focused on current needs, as speculative additions will likely require modification anyway. Infrastructure code or foundational abstractions that remain stable over time might warrant more comprehensive initial design, though even here, starting simple and adding complexity as needed often succeeds.

# Frequently changing business logic - keep simple
class OrderProcessor
  def process(order)
    order.status = :processing
    charge_payment(order)
    order.status = :completed
    send_confirmation(order)
  end
  
  private
  
  def charge_payment(order)
    PaymentGateway.charge(order.total)
  end
  
  def send_confirmation(order)
    OrderMailer.confirmation(order).deliver_later
  end
end

# vs over-engineered with speculative workflow engine
class OrderProcessor
  def initialize
    @workflow = WorkflowEngine.new do |w|
      w.state(:pending) { |o| transition_to_processing(o) }
      w.state(:processing) { |o| process_payment(o) }
      w.state(:payment_completed) { |o| send_notifications(o) }
      w.state(:completed) { |o| finalize(o) }
    end
  end
  
  def process(order)
    @workflow.execute(order)
  end
end

Team Experience: Teams experienced in refactoring and maintaining test coverage can apply YAGNI more aggressively than teams lacking these skills. The principle assumes that adding functionality later remains tractable. If the team struggles with modifying existing code or lacks confidence in their test coverage, they might compensate by building more upfront flexibility. However, this represents a team capability issue rather than a problem with YAGNI itself. Addressing the underlying capability through training and practice proves more sustainable than abandoning the principle.

Domain Complexity: Some domains exhibit intrinsic complexity that cannot be avoided. Financial systems must handle various transaction types, medical systems must accommodate regulatory requirements, and distributed systems must address failure modes. YAGNI does not recommend ignoring domain complexity, but rather advocates implementing complexity as requirements demand it. The distinction lies between addressing known domain complexity and speculating about future extensions.

Performance Characteristics: Performance optimization represents a common area where developers violate YAGNI by implementing optimizations before measurements demonstrate need. The principle recommends profiling first, then optimizing hotspots that actually impact user experience. Premature optimization creates complex code without verified benefit. However, some architectural decisions fundamentally impact performance and cannot be easily changed later. Algorithm selection, data structure choices, or database schema design might require consideration of performance characteristics during initial implementation.

Abstraction Levels: YAGNI applies differently at different abstraction levels. High-level architectural decisions warrant more consideration of future needs than low-level implementation details. An abstraction boundary between major system components might justify thought about potential future requirements, while the internal implementation of a single method should focus purely on current needs. The distinction relates to change cost and scope of impact.

Regulatory and Compliance: Systems subject to regulatory requirements, security audits, or compliance frameworks may require capabilities that appear speculative but reflect real constraints. Audit logging, data retention policies, or security controls might be mandated even before actively used. These represent real requirements driven by external constraints rather than imagined future features, and YAGNI does not recommend deferring them.

Ecosystem Maturity: Emerging technology ecosystems create uncertainty about patterns and best practices. In mature ecosystems with established patterns, YAGNI can be applied more confidently because standard solutions exist for common problems. In immature ecosystems, some exploratory development might prove necessary to understand the domain. However, even here, exploration should aim to answer specific questions rather than building comprehensive frameworks for hypothetical scenarios.

Ruby Implementation

Ruby's expressiveness and metaprogramming capabilities create particular challenges for applying YAGNI. The language makes it easy to build abstractions, which can tempt developers toward premature generalization. Ruby code demonstrating YAGNI typically favors direct implementation over clever abstraction, explicit logic over metaprogramming, and simple objects over complex frameworks.

Direct Methods vs Metaprogramming: Ruby's metaprogramming features allow generating methods dynamically, but this power often creates complexity that current requirements don't justify. YAGNI recommends defining methods explicitly until patterns emerge that justify metaprogramming.

# Violates YAGNI - metaprogramming for two similar methods
class Report
  [:daily, :weekly, :monthly].each do |period|
    define_method("#{period}_sales") do
      calculate_sales(period)
    end
  end
  
  private
  
  def calculate_sales(period)
    # implementation
  end
end

# Follows YAGNI - explicit methods for current needs
class Report
  def daily_sales
    sales_between(Date.today.beginning_of_day, Date.today.end_of_day)
  end
  
  def weekly_sales
    sales_between(Date.today.beginning_of_week, Date.today.end_of_week)
  end
  
  private
  
  def sales_between(start_time, end_time)
    Order.where(created_at: start_time..end_time).sum(:total)
  end
end

Single Class vs Abstraction Hierarchy: Ruby supports inheritance and modules, but not every design requires them. Starting with a single class implementing required behavior proves simpler than creating abstract base classes or mixins for anticipated variations.

# Violates YAGNI - abstract hierarchy for single implementation
class PaymentProcessor
  def process(payment)
    raise NotImplementedError
  end
end

class CreditCardProcessor < PaymentProcessor
  def process(payment)
    # credit card logic
  end
end

# Alternative with module for imagined future processors
module Payable
  def validate_payment
    raise NotImplementedError
  end
  
  def charge_customer
    raise NotImplementedError
  end
end

# Follows YAGNI - single class for current need
class PaymentProcessor
  def process(payment)
    return false unless payment.valid?
    
    response = CreditCardGateway.charge(
      card_number: payment.card_number,
      amount: payment.amount
    )
    
    payment.status = response.success? ? :completed : :failed
    payment.save
  end
end

Configuration vs Hardcoded Values: Ruby makes configuration systems easy to build, but YAGNI recommends hardcoding values until variation becomes necessary. Configuration introduces indirection and testing complexity without providing value until multiple configurations are actually needed.

# Violates YAGNI - comprehensive configuration
class EmailNotifier
  def initialize(config = {})
    @from_address = config[:from] || ENV['DEFAULT_FROM_EMAIL']
    @reply_to = config[:reply_to]
    @delivery_method = config[:delivery_method] || :smtp
    @template_engine = config[:template_engine] || :erb
    @async = config[:async] || false
  end
  
  def notify(user, message)
    email = build_email(user, message)
    deliver(email)
  end
end

# Follows YAGNI - hardcoded implementation
class EmailNotifier
  FROM_ADDRESS = "notifications@example.com"
  
  def notify(user, message)
    UserMailer.notification(user, message).deliver_later
  end
end

Rails Concerns and Modules: Rails encourages using concerns for shared behavior, but creating concerns before patterns emerge violates YAGNI. Duplicate code in two places doesn't automatically justify abstraction. Wait for three or more instances before extracting concerns.

# Violates YAGNI - concern for two models
module Taggable
  extend ActiveSupport::Concern
  
  included do
    has_many :taggings, as: :taggable
    has_many :tags, through: :taggings
  end
  
  def tag_names
    tags.pluck(:name)
  end
  
  def add_tag(name)
    tags << Tag.find_or_create_by(name: name)
  end
end

class Article < ApplicationRecord
  include Taggable
end

class Video < ApplicationRecord
  include Taggable
end

# Follows YAGNI - direct implementation until pattern emerges
class Article < ApplicationRecord
  has_many :article_tags
  has_many :tags, through: :article_tags
  
  def tag_names
    tags.pluck(:name)
  end
end

class Video < ApplicationRecord
  has_many :video_tags
  has_many :tags, through: :video_tags
  
  def tag_names
    tags.pluck(:name)
  end
end

Service Objects vs Active Record Callbacks: Rails developers often debate between callbacks and service objects. YAGNI suggests starting with callbacks for simple operations, moving to service objects when business logic complexity demands it, rather than preemptively building service layers.

# Violates YAGNI - service objects for simple operations
class Users::CreateService
  def initialize(params)
    @params = params
  end
  
  def call
    user = User.new(@params)
    return ServiceResult.failure(user.errors) unless user.valid?
    
    user.save
    send_welcome_email(user)
    ServiceResult.success(user)
  end
  
  private
  
  def send_welcome_email(user)
    WelcomeMailer.welcome(user).deliver_later
  end
end

# Follows YAGNI - callback for simple operation
class User < ApplicationRecord
  after_create :send_welcome_email
  
  private
  
  def send_welcome_email
    WelcomeMailer.welcome(self).deliver_later
  end
end

Database Schema Design: Ruby migrations make schema changes relatively painless in development. YAGNI recommends starting with simple schemas and adding indexes, constraints, or columns when needed rather than anticipating future requirements.

# Violates YAGNI - comprehensive schema for simple feature
class CreateUsers < ActiveRecord::Migration[7.0]
  def change
    create_table :users do |t|
      t.string :email, null: false, index: { unique: true }
      t.string :encrypted_password
      t.string :first_name
      t.string :last_name
      t.string :phone
      t.string :address_line1
      t.string :address_line2
      t.string :city
      t.string :state
      t.string :postal_code
      t.string :country, default: 'US'
      t.date :birth_date
      t.string :preferred_language, default: 'en'
      t.string :timezone, default: 'UTC'
      t.boolean :email_verified, default: false
      t.boolean :phone_verified, default: false
      t.integer :login_count, default: 0
      t.datetime :last_login_at
      t.timestamps
    end
    
    add_index :users, :email
    add_index :users, :last_login_at
    add_index :users, [:state, :city]
  end
end

# Follows YAGNI - minimal schema for authentication
class CreateUsers < ActiveRecord::Migration[7.0]
  def change
    create_table :users do |t|
      t.string :email, null: false
      t.string :encrypted_password
      t.timestamps
    end
    
    add_index :users, :email, unique: true
  end
end

API Design: Ruby's flexible method signatures enable building APIs that handle multiple scenarios. YAGNI recommends focused methods that solve current problems rather than flexible methods that handle hypothetical cases.

# Violates YAGNI - flexible API for single use case
class SearchService
  def search(query, options = {})
    results = base_query(query)
    results = apply_filters(results, options[:filters]) if options[:filters]
    results = apply_sorting(results, options[:sort]) if options[:sort]
    results = paginate(results, options[:page], options[:per_page]) if options[:page]
    results = include_associations(results, options[:include]) if options[:include]
    results
  end
end

# Follows YAGNI - focused method
class SearchService
  def search(query)
    Product.where("name ILIKE ?", "%#{query}%").limit(20)
  end
end

Gem Dependencies: Ruby gems provide extensive functionality, but including gems for anticipated features violates YAGNI. Add dependencies when features requiring them are implemented, not before. Each gem adds maintenance burden and potential security exposure.

Practical Examples

Real-world scenarios demonstrate YAGNI principles across different development contexts. These examples contrast YAGNI-compliant approaches with common violations, showing how speculative development creates unnecessary complexity.

Example 1: API Versioning

A startup builds its first API and immediately implements comprehensive versioning infrastructure including version negotiation, multiple format support, and deprecation handling. The application has three endpoints and no paying customers yet.

# Violates YAGNI - comprehensive versioning system
class ApiController < ApplicationController
  before_action :negotiate_version
  before_action :check_deprecated_version
  
  def negotiate_version
    @api_version = request.headers['Api-Version'] || 
                   params[:version] || 
                   extract_version_from_accept_header ||
                   current_api_version
    
    unless supported_versions.include?(@api_version)
      render json: { error: "Unsupported API version" }, status: 400
    end
  end
  
  def render_json(data)
    serializer = "Api::V#{@api_version}::#{controller_name.classify}Serializer"
    render json: serializer.constantize.new(data).as_json
  end
end

# Follows YAGNI - single version implementation
class ApiController < ApplicationController
  def index
    products = Product.all
    render json: products.as_json(only: [:id, :name, :price])
  end
end

The YAGNI approach implements a working API immediately. When version 2 becomes necessary, the team can add versioning based on actual requirements and lessons learned from API usage patterns. The speculative versioning system consumed development time, added testing complexity, and made assumptions about future versioning needs that may prove incorrect.

Example 2: Reporting System

A SaaS application needs basic usage reporting for customer dashboards. A developer builds a comprehensive reporting engine with scheduled jobs, data warehousing, custom query language, and export in seven formats.

# Violates YAGNI - complex reporting engine
class ReportingEngine
  SUPPORTED_FORMATS = [:pdf, :csv, :xlsx, :json, :xml, :html, :txt]
  
  def initialize
    @query_parser = QueryLanguageParser.new
    @data_warehouse = DataWarehouse.connection
    @scheduler = ReportScheduler.new
  end
  
  def generate_report(definition)
    query = @query_parser.parse(definition.query_string)
    data = @data_warehouse.execute(query)
    formatter = FormatterFactory.create(definition.format)
    formatter.format(data, definition.options)
  end
  
  def schedule_report(definition, schedule)
    @scheduler.add(definition, schedule)
  end
end

# Follows YAGNI - direct implementation
class UsageReport
  def self.for_customer(customer)
    {
      total_requests: customer.api_requests.count,
      requests_this_month: customer.api_requests
        .where('created_at >= ?', Date.today.beginning_of_month)
        .count,
      average_response_time: customer.api_requests
        .average(:response_time_ms)
    }
  end
end

# In controller
def show
  @usage = UsageReport.for_customer(current_customer)
  render json: @usage
end

The YAGNI implementation provides needed visibility immediately. As actual reporting requirements emerge, the system can evolve. Most of the comprehensive reporting features would never be used, and those that are needed would likely require modifications anyway based on how customers actually want to view their data.

Example 3: Authentication System

An application requires basic email/password authentication. Instead of implementing this directly, a developer builds an extensible authentication framework supporting multiple providers, two-factor authentication, SSO, and biometric authentication.

# Violates YAGNI - pluggable authentication framework
class AuthenticationFramework
  def initialize
    @strategies = {}
    register_default_strategies
  end
  
  def register_strategy(name, strategy_class)
    @strategies[name] = strategy_class
  end
  
  def authenticate(credentials)
    strategy = @strategies[credentials[:strategy]] || @strategies[:default]
    strategy.new.authenticate(credentials)
  end
end

class EmailPasswordStrategy < AuthenticationStrategy
  def authenticate(credentials)
    user = User.find_by(email: credentials[:email])
    return AuthenticationResult.failure unless user
    return AuthenticationResult.failure unless user.authenticate(credentials[:password])
    
    if credentials[:two_factor_code]
      verify_two_factor(user, credentials[:two_factor_code])
    else
      AuthenticationResult.success(user)
    end
  end
end

# Follows YAGNI - straightforward implementation
class SessionsController < ApplicationController
  def create
    user = User.find_by(email: params[:email])
    
    if user&.authenticate(params[:password])
      session[:user_id] = user.id
      redirect_to root_path
    else
      flash[:error] = "Invalid email or password"
      render :new
    end
  end
end

The simple implementation handles authentication requirements immediately. When SSO or two-factor authentication becomes necessary, the implementation can evolve. The framework added significant complexity with no immediate benefit and made assumptions about future authentication needs that may not match actual requirements when they arrive.

Example 4: File Upload Processing

An application needs basic image uploads for user profiles. A developer implements an abstract file processing pipeline with virus scanning, format conversion, cloud provider abstraction, and automatic thumbnail generation in five sizes.

# Violates YAGNI - abstract processing pipeline
class FileProcessor
  def initialize
    @pipeline = ProcessingPipeline.new
    configure_pipeline
  end
  
  def configure_pipeline
    @pipeline.add_stage(VirusScanStage.new)
    @pipeline.add_stage(FormatDetectionStage.new)
    @pipeline.add_stage(FormatConversionStage.new)
    @pipeline.add_stage(ThumbnailGenerationStage.new(sizes: [50, 100, 200, 400, 800]))
    @pipeline.add_stage(CloudStorageStage.new(provider: StorageProvider.current))
  end
  
  def process(file)
    @pipeline.execute(ProcessingContext.new(file))
  end
end

# Follows YAGNI - direct implementation
class User < ApplicationRecord
  has_one_attached :avatar
  
  def avatar_url
    avatar.attached? ? avatar : default_avatar_url
  end
  
  private
  
  def default_avatar_url
    "/images/default_avatar.png"
  end
end

# In controller
def update
  if current_user.update(user_params)
    redirect_to profile_path
  else
    render :edit
  end
end

private

def user_params
  params.require(:user).permit(:name, :email, :avatar)
end

The simple implementation using Rails Active Storage handles the requirement. Thumbnail generation can be added when needed, virus scanning when security requirements demand it, and cloud provider abstraction if switching providers becomes necessary. The pipeline architecture created complexity addressing hypothetical scenarios.

Example 5: Feature Flags

An application adds its first A/B test. Instead of implementing the specific test, a developer builds a comprehensive feature flag system with percentage rollouts, user targeting, flag dependencies, and audit logging.

# Violates YAGNI - comprehensive feature flag system
class FeatureFlagService
  def initialize
    @flags = FeatureFlag.all.index_by(&:key)
    @cache = Rails.cache
  end
  
  def enabled?(flag_key, user)
    flag = @flags[flag_key]
    return false unless flag
    
    return false unless flag.active?
    return false if flag.requires.any? { |dep| !enabled?(dep, user) }
    return true if user.in?(flag.target_users)
    return false if user.in?(flag.exclude_users)
    
    user_percentage = (user.id % 100)
    user_percentage < flag.rollout_percentage
  end
end

# Follows YAGNI - direct implementation
class ApplicationController < ActionController::Base
  def show_new_checkout?
    return true if current_user.email.ends_with?('@company.com')
    current_user.id.even?
  end
end

# In view
<% if show_new_checkout? %>
  <%= render 'checkout/new_version' %>
<% else %>
  <%= render 'checkout/current_version' %>
<% end %>

The direct implementation enables A/B testing immediately. As feature flag needs grow, the system can evolve toward a proper feature flag service or integrate with a third-party solution. The comprehensive system front-loaded complexity for features that might never be needed, and its design assumptions might not match actual feature flag patterns when they emerge.

Common Patterns

Several patterns embody YAGNI principles by favoring simplicity and deferring complexity until requirements demand it. These patterns guide developers toward implementations that address current needs without over-engineering for speculative futures.

Start with Conditionals: When handling variations in behavior, start with simple conditional logic rather than immediately introducing strategy patterns, polymorphism, or plugin architectures. Conditionals make the variation explicit and easy to understand. Refactor to more sophisticated patterns only when the number of variations or their complexity justifies it.

# Pattern: Conditional before abstraction
class OrderProcessor
  def calculate_shipping(order)
    if order.express?
      order.weight * 5.0
    else
      order.weight * 2.0
    end
  end
end

# Only introduce strategy when variations increase
class OrderProcessor
  def initialize(shipping_calculator: StandardShipping.new)
    @shipping_calculator = shipping_calculator
  end
  
  def calculate_shipping(order)
    @shipping_calculator.calculate(order)
  end
end

Inline Before Extract: Keep related logic inline within a method until the code actually gets reused elsewhere. The second time similar logic appears, consider extraction. The third time confirms the pattern and justifies abstraction. Premature extraction creates unnecessary indirection and naming overhead.

# Pattern: Inline first
class ReportGenerator
  def generate_sales_report
    data = Order.completed
      .where('created_at >= ?', 1.month.ago)
      .group('DATE(created_at)')
      .sum(:total)
    
    format_report(data, 'Sales Report')
  end
  
  # Wait for second usage before extracting
  def generate_revenue_report
    data = Order.completed
      .where('created_at >= ?', 1.month.ago)
      .group('DATE(created_at)')
      .sum(:total)
    
    format_report(data, 'Revenue Report')
  end
  
  # Now the pattern is clear, extract
  def orders_last_month
    Order.completed.where('created_at >= ?', 1.month.ago)
  end
end

Hardcode Before Configure: Start with hardcoded values and constants. Add configuration only when multiple environments or deployments need different values. Configuration introduces indirection and testing complexity. Hardcoded values remain explicit and easy to find.

# Pattern: Hardcode first
class RateLimiter
  MAX_REQUESTS = 100
  WINDOW_SECONDS = 60
  
  def exceeded?(user)
    requests = user.requests.where('created_at >= ?', WINDOW_SECONDS.seconds.ago)
    requests.count >= MAX_REQUESTS
  end
end

# Add configuration when needed
class RateLimiter
  def initialize(max_requests: 100, window_seconds: 60)
    @max_requests = max_requests
    @window_seconds = window_seconds
  end
  
  def exceeded?(user)
    requests = user.requests.where('created_at >= ?', @window_seconds.seconds.ago)
    requests.count >= @max_requests
  end
end

Specific Before Generic: Implement specific solutions for specific problems rather than generic frameworks. Generic solutions require understanding multiple use cases and anticipating variations. Specific solutions address known requirements with minimal complexity. Generalization becomes appropriate only after patterns emerge across multiple specific implementations.

# Pattern: Specific implementation
class OrderNotifier
  def notify_completed(order)
    OrderMailer.completion(order).deliver_later
  end
  
  def notify_shipped(order)
    OrderMailer.shipment(order).deliver_later
  end
end

# Avoid premature generalization
class EventNotifier
  def initialize
    @handlers = {}
  end
  
  def register_handler(event_type, handler)
    @handlers[event_type] = handler
  end
  
  def notify(event_type, subject)
    @handlers[event_type]&.call(subject)
  end
end

Manual Before Automated: Start with manual processes and automate only when repetition justifies automation cost. A script run occasionally by one person doesn't need scheduling infrastructure, monitoring, error handling, and logging. Automation adds complexity that only pays off at sufficient scale.

# Pattern: Manual script first
# scripts/cleanup_old_records.rb
require_relative '../config/environment'

puts "Deleting records older than 90 days..."
count = OldRecord.where('created_at < ?', 90.days.ago).delete_all
puts "Deleted #{count} records"

# Add automation when frequency justifies it
class RecordCleanupJob < ApplicationJob
  queue_as :low_priority
  
  def perform
    count = OldRecord.where('created_at < ?', 90.days.ago).delete_all
    Rails.logger.info "Cleanup job deleted #{count} records"
  end
end

Duplication Before Abstraction: Tolerate some duplication before introducing abstractions. Two similar pieces of code don't justify extraction. Three instances suggest a pattern worth abstracting. This pattern prevents premature abstraction based on coincidental similarity.

# Pattern: Accept duplication temporarily
class User < ApplicationRecord
  def full_name
    "#{first_name} #{last_name}"
  end
end

class Company < ApplicationRecord
  def full_name
    "#{name} #{legal_suffix}"
  end
end

# Similar method names don't mean shared abstraction is needed
# These serve different domains and may evolve differently
# Only extract if three or more models need identical logic

Standard Library Before Dependencies: Use standard library features before adding external dependencies. Each dependency adds maintenance burden, security exposure, and deployment complexity. Ruby's standard library provides substantial functionality. External dependencies become appropriate when they provide significant value beyond standard library capabilities.

# Pattern: Standard library first
require 'json'
require 'net/http'

class ApiClient
  def fetch_data(url)
    uri = URI(url)
    response = Net::HTTP.get_response(uri)
    JSON.parse(response.body)
  end
end

# Add dependency for complex needs
require 'faraday'
require 'faraday/retry'

class ApiClient
  def initialize
    @client = Faraday.new do |f|
      f.request :retry, max: 3
      f.response :logger
      f.adapter Faraday.default_adapter
    end
  end
end

Common Pitfalls

Applying YAGNI incorrectly creates problems as severe as ignoring it entirely. Understanding common misapplications helps developers distinguish between prudent simplicity and counterproductive shortcuts.

Ignoring Known Immediate Requirements: YAGNI addresses speculative features, not verified requirements scheduled for immediate implementation. Developers sometimes use YAGNI to justify skipping work that belongs in current scope. If a requirement appears in current sprint plans with specific acceptance criteria, it isn't speculative. YAGNI doesn't mean ignoring requirements; it means distinguishing real requirements from imagined ones.

# Misapplying YAGNI - skipping required feature
class UserRegistration
  def create(email, password)
    User.create(email: email, password: password)
    # "We don't need email verification yet" - but requirements say we do
  end
end

# Correct - implementing actual requirement
class UserRegistration
  def create(email, password)
    user = User.create(email: email, password: password)
    send_verification_email(user)
    user
  end
  
  private
  
  def send_verification_email(user)
    token = user.generate_verification_token
    UserMailer.verification(user, token).deliver_later
  end
end

Neglecting Architecture: YAGNI applies primarily to features and abstractions, not fundamental architectural decisions. Some upfront architectural thought prevents costly rework. The distinction lies between architecture that serves current requirements and speculative features. A developer shouldn't use YAGNI to justify skipping database design consideration or ignoring security architecture.

Confusing YAGNI with Technical Debt: Skipping tests, writing unclear code, or avoiding necessary refactoring isn't YAGNI. These represent technical debt that impedes future development. YAGNI means avoiding unnecessary features, not avoiding necessary quality. Tests for current functionality, clear naming, and maintainable structure all serve present requirements.

Overcompensating for Uncertainty: Some developers interpret YAGNI as reason to make all decisions reversible, even when irreversibility carries minimal cost. This leads to excessive abstraction around decisions that don't need it. Not all decisions require contingency plans. Some choices can be made confidently based on current requirements and changed if needs shift.

# Overcompensating - abstracting simple choice
class EmailSender
  def initialize(sender: EmailSenderAdapter.new)
    @sender = sender
  end
  
  def send(to, subject, body)
    @sender.send_email(to, subject, body)
  end
end

class EmailSenderAdapter
  def send_email(to, subject, body)
    ActionMailer::Base.mail(to: to, subject: subject, body: body).deliver_now
  end
end

# Appropriate - direct implementation
class EmailSender
  def send(to, subject, body)
    ActionMailer::Base.mail(to: to, subject: subject, body: body).deliver_now
  end
end

Applying YAGNI to Team Learning: Sometimes implementing features or abstractions serves learning purposes even without immediate product need. A team exploring new technology or establishing coding patterns might build examples that wouldn't pass strict YAGNI evaluation. This represents investment in team capability rather than product features and follows different evaluation criteria.

Waiting Too Long: YAGNI recommends deferring features until needed, not until after they're needed. Some developers take YAGNI as license to always implement the minimum possible, even when expansion is imminent and known. This creates churn where features are built, then immediately rebuilt. The principle assumes reasonable judgment about timing.

Ignoring Irreversible Decisions: Some decisions carry high reversal costs: public API contracts, database schema changes in production, cryptographic choices, or data serialization formats. These warrant more upfront consideration even under YAGNI. The cost of changing later might genuinely exceed the cost of present complexity. This doesn't mean over-engineering, but rather appropriate consideration of implications.

Using YAGNI to Avoid Refactoring: As code evolves, it accumulates cruft that should be cleaned up. Developers sometimes invoke YAGNI to justify not refactoring code that has become complex through incremental changes. YAGNI doesn't protect messy code from cleanup. Refactoring to improve existing code quality serves current maintainability needs.

Misunderstanding Requirements Certainty: Developers often mistake speculative features for actual requirements because stakeholders mention them. A feature mentioned in passing differs from a feature with acceptance criteria and scheduled implementation. YAGNI requires distinguishing between these categories and implementing only against concrete, near-term requirements.

Applying YAGNI Inconsistently: Teams sometimes apply YAGNI rigorously to some domains while ignoring it in others based on developer preferences rather than principled evaluation. This creates inconsistent complexity across the codebase. YAGNI should be applied consistently based on requirement certainty, not developer comfort with particular abstraction styles.

Premature Optimization Disguised as YAGNI: Developers sometimes skip necessary error handling, validation, or logging claiming YAGNI, when these address current quality requirements. YAGNI doesn't justify omitting error handling because errors "might not happen" or skipping logging because "we haven't needed it yet." These address present needs for reliable, maintainable software.

# Misusing YAGNI to skip error handling
class PaymentProcessor
  def process(payment)
    # "We don't need error handling yet"
    response = Gateway.charge(payment.amount)
    payment.status = :completed
  end
end

# Appropriate - handling actual present needs
class PaymentProcessor
  def process(payment)
    return false unless payment.valid?
    
    begin
      response = Gateway.charge(payment.amount)
      payment.status = response.success? ? :completed : :failed
      payment.save
    rescue Gateway::ConnectionError => e
      Rails.logger.error("Payment processing failed: #{e.message}")
      payment.status = :error
      false
    end
  end
end

Reference

YAGNI Decision Framework

Consideration Implement Now Defer Until Needed
Requirement Status Scheduled in current sprint Mentioned as possibility
Stakeholder Confirmation Written acceptance criteria Verbal speculation
Implementation Timeline Next few days Sometime in future
Usage Certainty Definite near-term use Might be needed eventually
Cost of Delay High change cost later Refactorable when needed
Domain Complexity Inherent complexity Anticipated complexity
Reversal Cost Expensive to change Cheap to refactor
Team Capability Limited refactoring skill Strong refactoring practice

Common YAGNI Scenarios

Scenario YAGNI Approach Over-Engineering
API Design Single version, focused endpoints Versioning system, format negotiation
Authentication Email and password Multiple provider framework
Configuration Hardcoded constants Configuration system
Data Export Single format needed Multiple format support
Error Handling Handle known errors Comprehensive error hierarchy
File Storage Local filesystem Cloud provider abstraction
Job Processing Simple background jobs Custom queue system
Logging Standard Rails logging Custom logging framework
Search Simple database query Full-text search engine
User Roles Admin and regular user Pluggable permissions system

Requirements Classification

Type Description YAGNI Application
Current In active development Implement immediately
Scheduled Committed for next iteration Consider preparation
Requested Stakeholder mentions Defer until scheduled
Anticipated Team predicts need Defer until requested
Possible Might someday be needed Ignore until discussed
Speculative Based on assumptions Never implement

Complexity Indicators

Signal Description Response
Multiple implementations Only one implementation exists YAGNI suggests waiting
Plugin architecture No plugins exist yet Defer abstraction
Configuration options Only one configuration used Remove configurability
Abstraction layers Abstracts single concrete class Inline abstraction
Framework code Supports hypothetical features Delete unused framework
Metaprogramming Generates methods dynamically Write methods explicitly
Generic methods Handle many scenarios Create specific methods
Future tense Code comments say "will need" Remove speculative code

Code Smell Identification

Pattern Description YAGNI Violation
Unused parameters Methods accepting unused parameters Parameters added for future use
Empty branches Conditional with empty else clause Placeholder for future logic
Abstract base classes No concrete implementations Framework for imagined subclasses
Plugin directories No plugins exist Infrastructure for potential plugins
Feature flags All features enabled Flags for unimplemented features
Version checks Single version exists Premature versioning
Configuration sections Sections with single value Speculative configuration
Rescue handlers Rescuing errors that can't occur Handling imagined failures

Refactoring Triggers

Trigger Description Action
Third instance Pattern appears three times Extract abstraction
Multiple configurations Two environments need different values Add configuration
Slow tests Tests take too long Add test doubles
Difficult changes Simple changes require extensive modifications Refactor structure
Unclear code Developers struggle to understand code Improve clarity
Second implementation Alternative approach needed Introduce strategy
Cross-cutting concern Same logic scattered across classes Extract module or concern
Complex conditional Many nested conditions Extract methods or polymorphism

Team Practices Supporting YAGNI

Practice Description Benefit
Test coverage Comprehensive test suite Enables safe refactoring
Continuous integration Automated test execution Validates refactoring
Code review Team reviews all changes Catches over-engineering
Refactoring skills Team comfortable with refactoring Reduces change fear
Short iterations Frequent delivery cycles Reveals actual requirements
Stakeholder collaboration Regular requirement clarification Distinguishes real from imagined needs
Retrospectives Regular practice evaluation Improves YAGNI application
Pair programming Real-time design discussion Prevents speculation