Understand the Acronym Spaghetti: From WTF to SOLID Code
Understand the Acronym Spaghetti: From WTF to SOLID Code
TLDR
- SRP: One responsibility per function
- DRY: Don't repeat code
- KISS: Keep it simple
- YAGNI: Don't build what you don't need
- Fail Fast: Catch errors early
- GIGO: Bad input = bad output
- Separation of Concerns: Keep different logic apart
Programming is full of acronyms. DRY, KISS, YAGNI, SOLID... it's like alphabet soup. But these aren't just fancy buzzwords - they're battle-tested principles that can transform your code from spaghetti mess to clean, maintainable software.
Let's break down the most important ones, sorted by how common they are, how important they are, and how hard they are to apply well.
The Foundation — Core Principles
These are the fundamentals. Use them everywhere.
SRP (Single Responsibility Principle)
Definition: Each function, class, or module should have only one reason to change.
Why it matters: Easier to test, maintain, and reuse.
When to break this rule: Very small, tightly coupled responsibilities can sometimes stay together. Don't create separate classes for every tiny operation.
Simple function example
// ❌ Does validation, saving, email, and logging all in one
const processUser = (userData) => {
if (!userData.email.includes('@')) throw new Error('Invalid email');
const user = database.save(userData);
emailService.sendWelcome(user);
logger.log('User processed');
return user;
};
// ✅ Same functionality, but each function has one job
const processUser = (userData) => {
validateUser(userData);
const user = saveUser(userData);
notifyUser(user);
return user;
};
React component example
// ❌ Component handles data fetching, validation, and rendering
const UserProfile = ({ userId }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(`/api/users/${userId}`).then(res => res.json()).then(setUser);
setLoading(false);
}, [userId]);
if (!user?.email?.includes('@')) return <div>Invalid user</div>;
return <div>{user.name}</div>;
};
// ✅ Same component, but separated concerns
const UserProfile = ({ userId }) => {
const { user, loading, error } = useUser(userId);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return <div>{user.name}</div>;
};
Common Pitfall: "Utility" classes that do everything.
DRY (Don't Repeat Yourself)
Definition: Every piece of knowledge should have a single, authoritative representation.
Why it matters: Fix bugs in one place, not ten. Consistency across your codebase.
When to break this rule: Some repetition is okay for organization (like page structure). General rule: if you have more than 2 copies, consider abstraction.
Email validation example
// ❌ Same validation logic repeated in different places
function validateEmail(email: string): boolean {
return email.includes('@') && email.includes('.') && email.length > 5;
}
function validateUserEmail(user: User): boolean {
return user.email.includes('@') && user.email.includes('.') && user.email.length > 5;
}
// ✅ Single source of truth, reused everywhere
function isValidEmail(email: string): boolean {
return email.includes('@') && email.includes('.') && email.length > 5;
}
function validateUserEmail(user: User): boolean {
return isValidEmail(user.email);
}
Common Pitfall: Copy-paste programming instead of extracting functions.
KISS (Keep It Simple, Stupid)
Also known as: Keep It Simple, Sweetie | Keep It Short and Simple
Definition: Choose the simplest solution that works. Avoid unnecessary complexity.
Why it matters: Simple code is easier to understand, debug, and maintain.
User search example
// ❌ Overly complex with unnecessary state management
const UserList = ({ users }) => {
const [filteredUsers, setFilteredUsers] = useState([]);
const [searchTerm, setSearchTerm] = useState('');
useEffect(() => {
const filtered = users.filter(user =>
user.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.email.toLowerCase().includes(searchTerm.toLowerCase())
);
setFilteredUsers(filtered);
}, [users, searchTerm]);
return (
<div>
<input onChange={e => setSearchTerm(e.target.value)} />
{filteredUsers.map(user => <UserCard key={user.id} user={user} />)}
</div>
);
};
// ✅ Simple, direct solution
const UserList = ({ users }) => {
const [search, setSearch] = useState('');
const filteredUsers = users.filter(user =>
user.name.includes(search) || user.email.includes(search)
);
return (
<div>
<input onChange={e => setSearch(e.target.value)} />
{filteredUsers.map(user => <UserCard key={user.id} user={user} />)}
</div>
);
};
Common Pitfall: Over-engineering solutions before you understand the real requirements.
YAGNI (You Aren't Gonna Need It)
Definition: Don't build features or abstractions until you actually need them.
Why it matters: Prevents wasted effort and keeps code simple. Requirements change.
Bad Example:
// ❌ Building for imaginary future requirements
interface User {
id: string;
name: string;
email: string;
// "Maybe we'll need these later"
phoneNumber?: string;
address?: Address;
preferences?: UserPreferences;
socialLinks?: SocialLink[];
customFields?: Record<string, any>;
}
const UserService = {
getUser: (id: string) => { /* fetch user */ },
getUserWithProfile: (id: string) => { /* complex join */ },
getUserWithPreferences: (id: string) => { /* another join */ },
getUserWithEverything: (id: string) => { /* kitchen sink */ },
// ... 15 more "just in case" methods
};
Good Example:
// ✅ Build only what you need now
interface User {
id: string;
name: string;
email: string;
}
const UserService = {
getUser: (id: string) => { /* fetch user */ },
updateUser: (id: string, data: Partial<User>) => { /* update user */ },
// Add more methods when actually needed
};
Common Pitfall: Adding configuration options, abstraction layers, or features "because we might need them someday."
Fail Fast
Definition: Detect and report errors as early as possible in the execution flow.
Why it matters: Easier debugging, prevents corruption, clearer error messages.
Division function example
// ❌ Returns Infinity or NaN instead of failing clearly
const divide = (a, b) => {
return a / b; // Silent failure - hard to debug
};
// ✅ Same function, but fails fast with clear error
const divide = (a, b) => {
if (b === 0) throw new Error('Division by zero');
return a / b;
};
React component example
// ❌ Component silently does nothing when userId is invalid
const UserProfile = ({ userId }) => {
const [user, setUser] = useState(null);
useEffect(() => {
if (userId) { // Silently skips if invalid
fetchUser(userId).then(setUser);
}
}, [userId]);
return <div>{user?.name || 'Loading...'}</div>; // Misleading
};
// ✅ Same component, but fails fast with clear error
const UserProfile = ({ userId }) => {
if (!userId) throw new Error('UserProfile requires a valid userId');
const [user, setUser] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
fetchUser(userId).catch(setError);
}, [userId]);
if (error) return <div>Error: {error.message}</div>;
return <div>{user?.name || 'Loading...'}</div>;
};
Common Pitfall: Using default values or ignoring validation errors instead of failing fast.
GIGO (Garbage In, Garbage Out)
Definition: The quality of your output is fundamentally limited by the quality of your input. Poor data leads to poor results.
Why it matters: You can't fix bad data with good code. Focus on data quality at the source.
When to break this rule: Sometimes you need to work with imperfect data and clean it as best you can rather than rejecting it.
The validation dilemma: Should a function validate its inputs or trust the caller? Rule of thumb: "Validate at boundaries, trust within boundaries." Public APIs and external interfaces should validate. Internal functions can trust that data has been cleaned upstream.
The GIGO perspective: Validating inside functions can violate GIGO if you're masking data quality problems instead of fixing them, silently correcting bad data without alerting anyone, or adding complexity to handle edge cases that shouldn't exist.
Average calculation example
// ❌ Processes bad data, returns wrong results
const calculateAverage = (numbers) => {
return numbers.reduce((sum, n) => sum + n, 0) / numbers.length;
// Returns NaN if array contains null, undefined, or strings
};
// ✅ Same function, but handles data quality
const calculateAverage = (numbers) => {
const validNumbers = numbers.filter(n => typeof n === 'number' && !isNaN(n));
if (validNumbers.length === 0) return 0;
return validNumbers.reduce((sum, n) => sum + n, 0) / validNumbers.length;
};
React component example
// ❌ Component displays misleading results from bad data
const UserStats = ({ users }) => {
const totalAge = users.reduce((sum, user) => sum + user.age, 0);
const averageAge = totalAge / users.length;
return <div>Average age: {averageAge}</div>;
// Shows "NaN" or wrong number if some users have age: null
};
// ✅ Same component, but handles data quality issues
const UserStats = ({ users }) => {
const usersWithValidAge = users.filter(user =>
typeof user.age === 'number' && user.age > 0
);
if (usersWithValidAge.length === 0) {
return <div>No valid age data available</div>;
}
const averageAge = calculateAverage(usersWithValidAge.map(u => u.age));
return <div>Average age: {averageAge.toFixed(1)} ({usersWithValidAge.length} users)</div>;
};
Common Pitfall: Thinking you can fix fundamental data quality issues with clever algorithms instead of addressing the root cause.
Separation of Concerns
Definition: Keep different responsibilities isolated from each other. Each part of your system should handle one distinct concern.
Why it matters: Easier to understand, test, and modify each part independently.
When to break this rule: Very small applications or prototypes where the overhead of separation isn't worth it yet.
User profile function example
// ❌ Mixes data access, business logic, and formatting
const getUserProfile = (userId) => {
const user = database.query('SELECT * FROM users WHERE id = ?', [userId]);
const fullName = user.firstName + ' ' + user.lastName;
const isAdult = user.age >= 18;
return {
name: fullName,
email: user.email,
canVote: isAdult,
displayColor: isAdult ? 'green' : 'red'
};
};
// ✅ Same functionality, but separated concerns
const getUserProfile = (userId) => {
const user = userRepository.findById(userId);
return userService.buildUserProfile(user);
};
// Each service handles one concern
const userRepository = {
findById: (id) => database.query('SELECT * FROM users WHERE id = ?', [id])
};
const userService = {
buildUserProfile: (user) => ({
name: `${user.firstName} ${user.lastName}`,
email: user.email,
canVote: user.age >= 18
})
};
React component example
// ❌ Component handles data fetching, business logic, and UI
const UserDashboard = ({ userId }) => {
const [user, setUser] = useState(null);
const [notifications, setNotifications] = useState([]);
useEffect(() => {
fetch(`/api/users/${userId}`).then(res => res.json()).then(setUser);
fetch(`/api/notifications/${userId}`).then(res => res.json()).then(setNotifications);
if (user?.lastLogin < Date.now() - 30 * 24 * 60 * 60 * 1000) {
sendReactivationEmail(user);
}
}, [userId]);
return (
<div style={{ backgroundColor: user?.isPremium ? 'gold' : 'white' }}>
<h1>{user?.firstName} {user?.lastName}</h1>
<p>Status: {user?.age >= 18 ? 'Adult' : 'Minor'}</p>
<div>{notifications.map(n => <p key={n.id}>{n.message}</p>)}</div>
</div>
);
};
// ✅ Same component, but separated concerns
const UserDashboard = ({ userId }) => {
const { user, loading } = useUser(userId);
const { notifications } = useNotifications(userId);
if (loading) return <LoadingSpinner />;
return <UserProfile user={user} notifications={notifications} />;
};
Common Pitfall: Creating too many layers of separation for simple operations, making code harder to follow.
Summary
That's The Foundation - the core principles every developer should master. These fundamentals will take you far in writing clean, maintainable code.
Coming next: Part 2 covers Code Quality & Safety principles, and Part 3 dives into SOLID Architecture patterns.
Remember: good code is not about showing off how smart you are - it's about making life easier for the next person who reads it (including future you).