
Clean Code Principles That Actually Matter (And Ones That Dont)
You’ve read the books. You’ve watched the talks. You’ve probably had “clean code” principles thrust upon you by a well-meaning but slightly zealous senior developer. And now you’re wondering: do I really need to limit every function to five lines? Is it actually a sin to use a comment? Will the programming gods strike me down if I use a global variable?
Let’s cut through the dogma and talk about which clean code principles genuinely improve your codebase—and which ones you can safely ignore.
The Clean Code Principles Worth Following
1. Names Should Reveal Intent
There’s nothing more frustrating than trying to understand code filled with cryptic abbreviations and generic names.
// BAD: What does this even do?
function calc(a, b, t) {
return t ? a + b : a * b;
}
// GOOD: Intent is clear from the name
function performMathOperation(firstNumber, secondNumber, shouldAdd) {
return shouldAdd ? firstNumber + secondNumber : firstNumber * secondNumber;
}
// EVEN BETTER: Two functions with crystal clear names
function add(first, second) {
return first + second;
}
function multiply(first, second) {
return first * second;
}
This principle pays dividends every single time someone reads your code—including future you. A few extra keystrokes now save hours of confusion later.
2. Functions Should Do One Thing
The “Single Responsibility Principle” is popular for a reason: functions that do exactly one thing are easier to test, reuse, and understand.
# BAD: This function does too many things
def process_user_registration(user_data):
# Validate email format
if not re.match(r"[^@]+@[^@]+\.[^@]+", user_data["email"]):
raise ValueError("Invalid email")
# Check if user exists
existing_user = db.query(User).filter_by(email=user_data["email"]).first()
if existing_user:
raise ValueError("User already exists")
# Hash password
hashed_password = bcrypt.hashpw(
user_data["password"].encode("utf-8"), bcrypt.gensalt()
)
# Create user
new_user = User(
email=user_data["email"],
password=hashed_password,
name=user_data["name"]
)
# Save to database
db.add(new_user)
db.commit()
# Send welcome email
send_email(
to=user_data["email"],
subject="Welcome!",
body="Thanks for registering..."
)
return new_user
# GOOD: Split into focused functions
def validate_email(email):
if not re.match(r"[^@]+@[^@]+\.[^@]+", email):
raise ValueError("Invalid email")
def check_user_exists(email):
existing_user = db.query(User).filter_by(email=email).first()
if existing_user:
raise ValueError("User already exists")
def hash_password(password):
return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt())
def create_user(email, name, hashed_password):
new_user = User(email=email, password=hashed_password, name=name)
db.add(new_user)
db.commit()
return new_user
def send_welcome_email(email):
send_email(
to=email,
subject="Welcome!",
body="Thanks for registering..."
)
def register_user(user_data):
validate_email(user_data["email"])
check_user_exists(user_data["email"])
hashed_password = hash_password(user_data["password"])
new_user = create_user(user_data["email"], user_data["name"], hashed_password)
send_welcome_email(user_data["email"])
return new_user
The refactored version has more functions, but each is testable and reusable. When a function does one thing, its name can accurately describe its purpose.
3. Early Returns Over Deep Nesting
Deep nesting creates code that’s hard to follow. Early returns keep the main logic path clean.
// BAD: Pyramid of doom
function processPayment(payment) {
if (payment) {
if (payment.amount > 0) {
if (payment.creditCard) {
if (isValidCreditCard(payment.creditCard)) {
// Process the payment
chargeCard(payment.creditCard, payment.amount);
return "Payment successful";
} else {
return "Invalid credit card";
}
} else {
return "No credit card provided";
}
} else {
return "Payment amount must be greater than 0";
}
} else {
return "No payment provided";
}
}
// GOOD: Early returns make the happy path obvious
function processPayment(payment) {
if (!payment) {
return "No payment provided";
}
if (payment.amount <= 0) {
return "Payment amount must be greater than 0";
}
if (!payment.creditCard) {
return "No credit card provided";
}
if (!isValidCreditCard(payment.creditCard)) {
return "Invalid credit card";
}
// Happy path is clear and unindented
chargeCard(payment.creditCard, payment.amount);
return "Payment successful";
}
4. Delete Dead Code
Code that’s commented out or flagged with “we might need this later” is a burden. It creates noise and confusion. Version control exists for a reason—use it.
function calculateTotal(items) {
return items.reduce((total, item) => {
return total + item.price * item.quantity;
}, 0);
// Old calculation method - keeping just in case
// let total = 0;
// for (let i = 0; i < items.length; i++) {
// total += items[i].price * items[i].quantity;
// }
// return total;
}
Delete the commented-out code. If you ever need it again, Git has your back.
5. Replace Magic Numbers With Named Constants
Magic numbers obscure the intent and make future changes risky.
// BAD: What do these numbers mean?
function calculateShipping(weight, distance) {
if (weight < 2) {
return distance * 0.5;
} else if (weight < 10) {
return distance * 1.25;
} else {
return distance * 2.15;
}
}
// GOOD: Constants explain the business logic
const WEIGHT_THRESHOLD_LIGHT = 2; // kg
const WEIGHT_THRESHOLD_MEDIUM = 10; // kg
const RATE_LIGHT_PACKAGE = 0.5; // $ per mile
const RATE_MEDIUM_PACKAGE = 1.25; // $ per mile
const RATE_HEAVY_PACKAGE = 2.15; // $ per mile
function calculateShipping(weight, distance) {
if (weight < WEIGHT_THRESHOLD_LIGHT) {
return distance * RATE_LIGHT_PACKAGE;
} else if (weight < WEIGHT_THRESHOLD_MEDIUM) {
return distance * RATE_MEDIUM_PACKAGE;
} else {
return distance * RATE_HEAVY_PACKAGE;
}
}
Now when shipping rates change (and they will), you update them in one place, and the meaning is clear.
Clean Code Principles You Can Ignore (Sometimes)
1. “Functions Should Be Small”
The dogma says functions should be 5-10 lines max. Reality says functions should be as long as needed to be self-contained and coherent.
# This violates the "small function" rule but is perfectly reasonable
def parse_config_file(file_path):
with open(file_path, 'r') as file:
lines = file.readlines()
config = {}
current_section = None
for line in lines:
line = line.strip()
# Skip empty lines and comments
if not line or line.startswith('#'):
continue
# Handle section headers
if line.startswith('[') and line.endswith(']'):
current_section = line[1:-1]
config[current_section] = {}
continue
# Handle key-value pairs
if '=' in line and current_section is not None:
key, value = line.split('=', 1)
config[current_section][key.strip()] = value.strip()
return config
This function is ~25 lines and does one coherent thing. Breaking it into 5-line functions would make it harder to follow, not easier.
2. “Never Use Comments”
The clean code absolutists say “if you need a comment, your code isn’t clear enough.” Reality is more nuanced.
// GOOD USE OF COMMENTS: Explaining "why" not "what"
function validatePassword(password) {
// NIST Special Publication 800-63B recommends passwords of at least 8 characters
// without complexity requirements or periodic changes
if (password.length < 8) {
return false;
}
// Company policy requires at least one number (despite NIST guidance)
// Issue #4328: Remove this requirement in Q3 2025
if (!/\d/.test(password)) {
return false;
}
return true;
}
Comments explaining why something is done a certain way (especially business rules, security decisions, or non-obvious performance optimizations) are valuable.
3. “Never Use Global State”
While global variables can cause problems, some things genuinely belong at a global level:
// Perfectly reasonable globals
const API_BASE_URL = 'https://api.example.com/v2';
const MAX_RETRY_ATTEMPTS = 3;
const DEFAULT_TIMEOUT_MS = 5000;
// Genuinely useful singletons
const logger = createLogger({ level: 'info' });
const database = createDatabaseConnection(config.database);
Configuration, environment settings, and legitimate singletons can safely be global.
4. “Always Use TDD”
Test-Driven Development is powerful but not always the most efficient approach. Sometimes it’s better to sketch out a solution first and then write tests.
// Approach 1: TDD (write tests first)
test('user can be created with valid email', () => {
const user = createUser('test@example.com', 'password');
expect(user.email).toBe('test@example.com');
});
test('creating user with invalid email throws error', () => {
expect(() => createUser('not-an-email', 'password')).toThrow();
});
// Then implement to make tests pass
// Approach 2: Implementation first, then tests
// This can be more efficient for exploratory programming
function createUser(email, password) {
if (!isValidEmail(email)) {
throw new Error('Invalid email');
}
// Implementation...
}
// Write tests after to verify behavior
Both approaches work. Use the one that fits your context.
5. “Never Use Exceptions for Control Flow”
This rule has merit but isn’t absolute. Sometimes exceptions are the cleanest way to handle certain conditions.
# Sometimes exceptions are cleaner than deeply nested checks
def get_config_value(config, section, key, default=None):
try:
return config[section][key]
except (KeyError, TypeError):
return default
This is cleaner than explicitly checking if section
exists in config
and then if key
exists in that section.
The Pragmatic Middle Ground
Clean code isn’t about following rules dogmatically; it’s about making code maintainable. Here’s how to be pragmatic:
Focus on Readability Above All
The ultimate test: Can someone else (or future you) read this code and understand what it does without excessive mental effort?
// Even if this breaks some "rules," it's clear and maintainable
function calculateTotalPrice(items, userType) {
let subtotal = 0;
// Sum all item prices
for (const item of items) {
subtotal += item.price * item.quantity;
}
// Apply appropriate discount based on user type
let discount = 0;
if (userType === 'premium') {
discount = subtotal * 0.1; // 10% discount for premium users
} else if (userType === 'loyal' && subtotal > 100) {
discount = subtotal * 0.05; // 5% for loyal customers on orders over $100
}
// Calculate tax (after discount)
const taxableAmount = subtotal - discount;
const tax = taxableAmount * 0.08; // 8% sales tax
return {
subtotal,
discount,
tax,
total: subtotal - discount + tax
};
}
This function might be longer than “ideal,” but it’s straightforward and does exactly what its name suggests.
Optimize for Change
Code that’s easy to change is more valuable than code that follows every rule.
// This violates "functions should only have one level of abstraction"
// But it puts all related business logic in one place
function processOrder(order) {
// Validate order items
for (const item of order.items) {
if (item.quantity <= 0) {
throw new Error(`Invalid quantity for ${item.name}`);
}
if (!isInStock(item.id, item.quantity)) {
throw new Error(`Insufficient stock for ${item.name}`);
}
}
// Calculate totals
const subtotal = calculateSubtotal(order.items);
const tax = calculateTax(subtotal, order.shippingAddress.state);
const shipping = calculateShipping(order.items, order.shippingMethod);
const total = subtotal + tax + shipping;
// Process payment
const paymentResult = processPayment(order.paymentDetails, total);
if (!paymentResult.success) {
throw new Error(`Payment failed: ${paymentResult.error}`);
}
// Update inventory and create shipment
updateInventory(order.items);
const shipment = createShipment(order.items, order.shippingAddress);
// Return order confirmation
return {
orderId: generateOrderId(),
total,
shipment,
estimatedDelivery: calculateEstimatedDelivery(shipment)
};
}
When business requirements change (and they will), having related logic in one place makes updates easier, even if the function is longer than “ideal.”
Conclusion
Clean code principles should serve you, not the other way around. The best programmers know when to follow the rules and when to break them.
Always focus on:
- Naming things clearly
- Making code easy to understand
- Reducing cognitive load for readers
- Making changes and fixes straightforward
But feel free to ignore any rule that makes your code less readable or harder to maintain in your specific context.
Remember that code is primarily written for humans to read, not just for computers to execute. The measure of code quality isn’t how many principles it follows, but how effectively it can be understood, maintained, and adapted over time.
Now go forth and write code that’s clean in the ways that actually matter.