YAGNI Principle
YAGNI Principle: You Aren’t Gonna Need It
Section titled “YAGNI Principle: You Aren’t Gonna Need It”The YAGNI principle (You Aren’t Gonna Need It) is a core principle of Extreme Programming (XP) that states: “Don’t add functionality until it’s actually needed.” Understanding the YAGNI principle is essential for avoiding over-engineering and building maintainable software.
Why YAGNI Principle?
Section titled “Why YAGNI Principle?”YAGNI Principle helps you:
- Avoid over-engineering - Don’t build features you don’t need
- Save time - Focus on what’s actually needed
- Reduce complexity - Less code to maintain
- Faster delivery - Ship working features sooner
- Flexibility - Add features when requirements are clear
Visual: YAGNI Principle Concept
Section titled “Visual: YAGNI Principle Concept”What Happens If We Don’t Follow YAGNI?
Section titled “What Happens If We Don’t Follow YAGNI?”Without YAGNI Principle, you might:
- Over-engineer - Build features that are never used
- Waste time - Spend time on unnecessary code
- Increase complexity - More code = more bugs
- Slower delivery - Takes longer to ship features
- Harder to change - Unused code gets in the way
Visual: The Over-Engineering Problem
Section titled “Visual: The Over-Engineering Problem”Simple Example: User Management System
Section titled “Simple Example: User Management System”Let’s see a simple example showing the problem and solution:
The Problem: Building for Future That May Never Come
Section titled “The Problem: Building for Future That May Never Come”Visual: Over-Engineering Flow
Section titled “Visual: Over-Engineering Flow”# ❌ Without YAGNI - Building features you don't need!
class User: """Over-engineered user class with features you don't need"""
def __init__(self, name: str, email: str): self.name = name self.email = email # Building for future that may never come self.preferences = {} # Don't need this yet! self.social_links = {} # Don't need this yet! self.subscription_tier = None # Don't need this yet! self.notification_settings = {} # Don't need this yet! self.analytics_data = {} # Don't need this yet!
def update_preferences(self, prefs: dict): # Feature you don't need yet! self.preferences.update(prefs)
def add_social_link(self, platform: str, url: str): # Feature you don't need yet! self.social_links[platform] = url
# ... many more methods for features you don't need
# Problems:# - Code is complex for no reason# - Harder to understand what's actually used# - Wasted time building unused features# - Harder to maintain unused code// ❌ Without YAGNI - Building features you don't need!
public class User { // Over-engineered user class with features you don't need private String name; private String email; // Building for future that may never come private Map<String, Object> preferences; // Don't need this yet! private Map<String, String> socialLinks; // Don't need this yet! private String subscriptionTier; // Don't need this yet! private Map<String, Boolean> notificationSettings; // Don't need this yet! private Map<String, Object> analyticsData; // Don't need this yet!
public User(String name, String email) { this.name = name; this.email = email; this.preferences = new HashMap<>(); this.socialLinks = new HashMap<>(); this.notificationSettings = new HashMap<>(); this.analyticsData = new HashMap<>(); }
public void updatePreferences(Map<String, Object> prefs) { // Feature you don't need yet! this.preferences.putAll(prefs); }
public void addSocialLink(String platform, String url) { // Feature you don't need yet! this.socialLinks.put(platform, url); }
// ... many more methods for features you don't need}
// Problems:// - Code is complex for no reason// - Harder to understand what's actually used// - Wasted time building unused features// - Harder to maintain unused code// ❌ Without YAGNI - Building features you don't need!
class User { /** Over-engineered user class with features you don't need */ name: string; email: string; // Building for future that may never come preferences: Record<string, any>; // Don't need this yet! socialLinks: Record<string, string>; // Don't need this yet! subscriptionTier: string | null; // Don't need this yet! notificationSettings: Record<string, boolean>; // Don't need this yet! analyticsData: Record<string, any>; // Don't need this yet!
constructor(name: string, email: string) { this.name = name; this.email = email; this.preferences = {}; this.socialLinks = {}; this.subscriptionTier = null; this.notificationSettings = {}; this.analyticsData = {}; }
updatePreferences(prefs: Record<string, any>): void { // Feature you don't need yet! this.preferences = { ...this.preferences, ...prefs }; }
addSocialLink(platform: string, url: string): void { // Feature you don't need yet! this.socialLinks[platform] = url; }
// ... many more methods for features you don't need}
// Problems:// - Code is complex for no reason// - Harder to understand what's actually used// - Wasted time building unused features// - Harder to maintain unused code// ❌ Without YAGNI - Building features you don't need!
#include <string>#include <map>#include <optional>
class User {private: // Over-engineered user class with features you don't need std::string name; std::string email; // Building for future that may never come std::map<std::string, std::string> preferences; // Don't need this yet! std::map<std::string, std::string> socialLinks; // Don't need this yet! std::optional<std::string> subscriptionTier; // Don't need this yet! std::map<std::string, bool> notificationSettings; // Don't need this yet! std::map<std::string, std::string> analyticsData; // Don't need this yet!
public: User(const std::string& name, const std::string& email) : name(name), email(email) {}
void updatePreferences(const std::map<std::string, std::string>& prefs) { // Feature you don't need yet! for (const auto& [key, value] : prefs) { preferences[key] = value; } }
void addSocialLink(const std::string& platform, const std::string& url) { // Feature you don't need yet! socialLinks[platform] = url; }
// ... many more methods for features you don't need};
// Problems:// - Code is complex for no reason// - Harder to understand what's actually used// - Wasted time building unused features// - Harder to maintain unused code// ❌ Without YAGNI - Building features you don't need!
using System.Collections.Generic;
public class User{ // Over-engineered user class with features you don't need public string Name { get; set; } public string Email { get; set; } // Building for future that may never come public Dictionary<string, object> Preferences { get; set; } // Don't need this yet! public Dictionary<string, string> SocialLinks { get; set; } // Don't need this yet! public string SubscriptionTier { get; set; } // Don't need this yet! public Dictionary<string, bool> NotificationSettings { get; set; } // Don't need this yet! public Dictionary<string, object> AnalyticsData { get; set; } // Don't need this yet!
public User(string name, string email) { Name = name; Email = email; Preferences = new Dictionary<string, object>(); SocialLinks = new Dictionary<string, string>(); NotificationSettings = new Dictionary<string, bool>(); AnalyticsData = new Dictionary<string, object>(); }
public void UpdatePreferences(Dictionary<string, object> prefs) { // Feature you don't need yet! foreach (var kvp in prefs) { Preferences[kvp.Key] = kvp.Value; } }
public void AddSocialLink(string platform, string url) { // Feature you don't need yet! SocialLinks[platform] = url; }
// ... many more methods for features you don't need}
// Problems:// - Code is complex for no reason// - Harder to understand what's actually used// - Wasted time building unused features// - Harder to maintain unused code// ❌ Without YAGNI — speculative features drive complexity
package main
import ( "fmt")
type OverEngineeredUser struct { Name, Email string Preferences map[string]any SocialLinks map[string]string SubscriptionTier string NotificationSettings map[string]bool AnalyticsData map[string]any}
func NewOverEngineeredUser(name, email string) *OverEngineeredUser { return &OverEngineeredUser{ Name: name, Email: email, Preferences: map[string]any{}, SocialLinks: map[string]string{}, NotificationSettings: map[string]bool{}, AnalyticsData: map[string]any{}, }}
func (u *OverEngineeredUser) UpdatePreferences(prefs map[string]any) { for k, v := range prefs { u.Preferences[k] = v } fmt.Println("implementing prefs before you truly need them")}
func (u *OverEngineeredUser) AddSocialLink(platform, url string) { u.SocialLinks[platform] = url}
// Same problems as the C# snippet: churn, noise, speculative surface area.// ❌ Without YAGNI — speculative features drive complexity
use std::collections::HashMap;
struct OverEngineeredUser { name: String, email: String, preferences: HashMap<String, String>, social_links: HashMap<String, String>, subscription_tier: Option<String>, notification_settings: HashMap<String, bool>, analytics_data: HashMap<String, String>,}
impl OverEngineeredUser { fn new(name: impl Into<String>, email: impl Into<String>) -> Self { Self { name: name.into(), email: email.into(), preferences: HashMap::new(), social_links: HashMap::new(), subscription_tier: None, notification_settings: HashMap::new(), analytics_data: HashMap::new(), } }
fn update_preferences(&mut self, prefs: HashMap<String, String>) { self.preferences.extend(prefs); println!("implementing prefs before you truly need them"); }
fn add_social_link(&mut self, platform: String, url: String) { self.social_links.insert(platform, url); }}
// Same problems: churn, noise, speculative surface area.The Solution: YAGNI Principle
Section titled “The Solution: YAGNI Principle”Visual: YAGNI Solution Flow
Section titled “Visual: YAGNI Solution Flow”# ✅ With YAGNI - Build only what you need!
class User: """Simple user class - only what you actually need"""
def __init__(self, name: str, email: str): self.name = name self.email = email # That's it! Only what you need right now
def get_name(self) -> str: return self.name
def get_email(self) -> str: return self.email
# When you actually need preferences, add them then!# When you actually need social links, add them then!# Don't build them "just in case"
# Benefits:# - Simple and clear# - Easy to understand# - Fast to implement# - Easy to maintain# - Can add features when actually needed// ✅ With YAGNI - Build only what you need!
public class User { // Simple user class - only what you actually need private String name; private String email;
public User(String name, String email) { this.name = name; this.email = email; // That's it! Only what you need right now }
public String getName() { return name; }
public String getEmail() { return email; }}
// When you actually need preferences, add them then!// When you actually need social links, add them then!// Don't build them "just in case"
// Benefits:// - Simple and clear// - Easy to understand// - Fast to implement// - Easy to maintain// - Can add features when actually needed// ✅ With YAGNI - Build only what you need!
class User { /** Simple user class - only what you actually need */ private name: string; private email: string;
constructor(name: string, email: string) { this.name = name; this.email = email; // That's it! Only what you need right now }
getName(): string { return this.name; }
getEmail(): string { return this.email; }}
// When you actually need preferences, add them then!// When you actually need social links, add them then!// Don't build them "just in case"
// Benefits:// - Simple and clear// - Easy to understand// - Fast to implement// - Easy to maintain// - Can add features when actually needed// ✅ With YAGNI - Build only what you need!
#include <string>
class User {private: // Simple user class - only what you actually need std::string name; std::string email;
public: User(const std::string& name, const std::string& email) : name(name), email(email) { // That's it! Only what you need right now }
std::string getName() const { return name; }
std::string getEmail() const { return email; }};
// When you actually need preferences, add them then!// When you actually need social links, add them then!// Don't build them "just in case"
// Benefits:// - Simple and clear// - Easy to understand// - Fast to implement// - Easy to maintain// - Can add features when actually needed// ✅ With YAGNI - Build only what you need!
public class User{ // Simple user class - only what you actually need private string name; private string email;
public User(string name, string email) { this.name = name; this.email = email; // That's it! Only what you need right now }
public string GetName() { return name; }
public string GetEmail() { return email; }}
// When you actually need preferences, add them then!// When you actually need social links, add them then!// Don't build them "just in case"
// Benefits:// - Simple and clear// - Easy to understand// - Fast to implement// - Easy to maintain// - Can add features when actually needed// ✅ With YAGNI — minimal user model
package main
type User struct { name, email string}
func NewUser(name, email string) *User { return &User{name: name, email: email}}
func (u *User) GetName() string { return u.name }func (u *User) GetEmail() string { return u.email }
// Add preferences or social graph only when a real requirement shows up.// ✅ With YAGNI — minimal user model
struct User { name: String, email: String,}
impl User { fn new(name: impl Into<String>, email: impl Into<String>) -> Self { Self { name: name.into(), email: email.into(), } }
fn name(&self) -> &str { &self.name }
fn email(&self) -> &str { &self.email }}
// Add preferences or social graph only when a real requirement shows up.Real-World Example: API Design
Section titled “Real-World Example: API Design”Here’s a more realistic example:
# ❌ Without YAGNI - Building API endpoints you don't need
class UserAPI: """Over-engineered API with endpoints you don't need"""
def get_user(self, user_id: int): # You need this pass
def create_user(self, user_data: dict): # You need this pass
def update_user_preferences(self, user_id: int, prefs: dict): # You don't need this yet! pass
def get_user_analytics(self, user_id: int): # You don't need this yet! pass
def export_user_data(self, user_id: int): # You don't need this yet! pass
# ✅ With YAGNI - Build only endpoints you need
class UserAPI: """Simple API - only what you actually need"""
def get_user(self, user_id: int): # You need this - build it! pass
def create_user(self, user_data: dict): # You need this - build it! pass
# Don't build other endpoints until you actually need them! # When you need them, add them then.// ❌ Without YAGNI - Building API endpoints you don't need
public class UserAPI { // Over-engineered API with endpoints you don't need public User getUser(int userId) { // You need this return null; }
public User createUser(UserData userData) { // You need this return null; }
public void updateUserPreferences(int userId, Map<String, Object> prefs) { // You don't need this yet! }
public Map<String, Object> getUserAnalytics(int userId) { // You don't need this yet! }
public byte[] exportUserData(int userId) { // You don't need this yet! }}
// ✅ With YAGNI - Build only endpoints you need
public class UserAPI { // Simple API - only what you actually need public User getUser(int userId) { // You need this - build it! return null; }
public User createUser(UserData userData) { // You need this - build it! return null; }
// Don't build other endpoints until you actually need them! // When you need them, add them then.}// ❌ Without YAGNI - Building API endpoints you don't need
class UserAPI { /** Over-engineered API with endpoints you don't need */
getUser(userId: number): User | null { // You need this return null; }
createUser(userData: UserData): User | null { // You need this return null; }
updateUserPreferences(userId: number, prefs: Record<string, any>): void { // You don't need this yet! }
getUserAnalytics(userId: number): Record<string, any> | null { // You don't need this yet! return null; }
exportUserData(userId: number): Uint8Array | null { // You don't need this yet! return null; }}
// ✅ With YAGNI - Build only endpoints you need
class UserAPI { /** Simple API - only what you actually need */
getUser(userId: number): User | null { // You need this - build it! return null; }
createUser(userData: UserData): User | null { // You need this - build it! return null; }
// Don't build other endpoints until you actually need them! // When you need them, add them then.}// ❌ Without YAGNI - Building API endpoints you don't need
#include <string>#include <map>#include <vector>
class UserAPI {public: // Over-engineered API with endpoints you don't need User* getUser(int userId) { // You need this return nullptr; }
User* createUser(const UserData& userData) { // You need this return nullptr; }
void updateUserPreferences(int userId, const std::map<std::string, std::string>& prefs) { // You don't need this yet! }
std::map<std::string, std::string> getUserAnalytics(int userId) { // You don't need this yet! return {}; }
std::vector<uint8_t> exportUserData(int userId) { // You don't need this yet! return {}; }};
// ✅ With YAGNI - Build only endpoints you need
class UserAPI {public: // Simple API - only what you actually need User* getUser(int userId) { // You need this - build it! return nullptr; }
User* createUser(const UserData& userData) { // You need this - build it! return nullptr; }
// Don't build other endpoints until you actually need them! // When you need them, add them then.};// ❌ Without YAGNI - Building API endpoints you don't need
using System.Collections.Generic;
public class UserAPI{ // Over-engineered API with endpoints you don't need public User GetUser(int userId) { // You need this return null; }
public User CreateUser(UserData userData) { // You need this return null; }
public void UpdateUserPreferences(int userId, Dictionary<string, object> prefs) { // You don't need this yet! }
public Dictionary<string, object> GetUserAnalytics(int userId) { // You don't need this yet! return null; }
public byte[] ExportUserData(int userId) { // You don't need this yet! return null; }}
// ✅ With YAGNI - Build only endpoints you need
public class UserAPI{ // Simple API - only what you actually need public User GetUser(int userId) { // You need this - build it! return null; }
public User CreateUser(UserData userData) { // You need this - build it! return null; }
// Don't build other endpoints until you actually need them! // When you need them, add them then.}// ❌ Without YAGNI — shipping endpoints “just in case”
package demo
type User struct{}type UserData struct{}
type UserAPIHeavy struct{}
func (*UserAPIHeavy) GetUser(userID int) *User { return nil }func (*UserAPIHeavy) CreateUser(ud UserData) *User { return nil }func (*UserAPIHeavy) UpdateUserPreferences(userID int, prefs map[string]any) {}func (*UserAPIHeavy) GetUserAnalytics(userID int) map[string]any { return nil }func (*UserAPIHeavy) ExportUserData(userID int) []byte { return nil }
// ✅ With YAGNI — expose only the routes you need today
type UserAPI struct{}
func (*UserAPI) GetUser(userID int) *User { return nil }func (*UserAPI) CreateUser(ud UserData) *User { return nil }
// Extend the struct with new methods only when a requirement arrives.// ❌ Without YAGNI — shipping endpoints "just in case"
struct User;struct UserData;
struct UserApiHeavy;
impl UserApiHeavy { fn get_user(&self, _user_id: i32) -> Option<User> { None } fn create_user(&self, _data: UserData) -> User { User } fn update_user_preferences(&self, _user_id: i32, _prefs: Vec<(String, String)>) {} fn get_user_analytics(&self, _user_id: i32) -> Vec<(String, String)> { Vec::new() } fn export_user_data(&self, _user_id: i32) -> Vec<u8> { Vec::new() }}
// ✅ With YAGNI — expose only the routes you need today
struct UserApi;
impl UserApi { fn get_user(&self, _user_id: i32) -> Option<User> { None } fn create_user(&self, _data: UserData) -> User { User }}
// Extend the impl with new methods only when a requirement arrives.When to Apply YAGNI?
Section titled “When to Apply YAGNI?”Apply YAGNI Principle when:
✅ Building new features - Only build what’s needed now
✅ Designing abstractions - Don’t abstract until you see duplication
✅ Adding “nice to have” features - Skip them until actually needed
✅ Premature optimization - Don’t optimize until you have performance issues
✅ Future-proofing - Don’t build for hypothetical future needs
Visual: When to Apply YAGNI
Section titled “Visual: When to Apply YAGNI”When NOT to Apply YAGNI?
Section titled “When NOT to Apply YAGNI?”Don’t apply YAGNI when:
❌ Clear requirements - If you know you’ll need it soon, build it
❌ Critical infrastructure - Some things need to be built right
❌ Security concerns - Security features should be built proactively
❌ Technical debt - Sometimes you need to fix architecture issues
Key Takeaways
Section titled “Key Takeaways”- YAGNI Principle = You Aren’t Gonna Need It
- Build only what you need - Don’t add functionality until needed
- Avoid over-engineering - Don’t build “just in case”
- Faster delivery - Ship working features sooner
- Balance - Don’t confuse YAGNI with poor planning
Remember: YAGNI is about avoiding unnecessary features, not avoiding planning. Build what you need, when you need it! 🎯