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 |