Let me start with a confession that might make some of you reach for the pitchforks: I’ve shipped production code with global variables. I’ve committed directly to main. I’ve written functions longer than a CVS receipt. And you know what? Sometimes it was the right choice. Before you close this tab and question my sanity, hear me out. The software development world has become obsessed with best practices to the point where we’ve created a new form of cargo cult programming. We follow rules religiously without understanding their context, turning guidelines into gospel and flexibility into heresy.
The Best Practice Trap
The irony of best practices is that they often become worst practices when applied blindly. Every “don’t do this” rule was created by someone who probably did exactly that thing to solve a specific problem in a specific context. Yet we’ve somehow convinced ourselves that context doesn’t matter anymore. Consider this: the developers who created the practices we now worship as “best” probably broke every rule in the book to get there. Innovation doesn’t come from following the crowd – it comes from knowing when to break away from it.
When Rules Become Shackles
The Prototype Paradox
You’re building a proof of concept to test a revolutionary algorithm that could change your company’s trajectory. Your manager needs to see results by Friday, and you’re spending Tuesday refactoring your validation functions to follow SOLID principles. Stop. Just stop. Here’s what prototype code should look like:
# Yes, this breaks every naming convention
# Yes, this has magic numbers
# No, I don't care right now
def quick_and_dirty_validation(data):
if len(data) < 100: # Magic number? Sue me.
return False
# Global variable for tracking - fight me
global processed_count
processed_count += 1
# One giant function doing everything
result = data.transform().validate().normalize().score()
# Hardcoded threshold because it's a prototype
return result > 0.75
# This works, it's fast to write, and it proves the concept
The alternative? Spending three days creating abstract factories and dependency injection containers for code that might get thrown away tomorrow. Sometimes the best practice is to ignore best practices entirely.
The Startup Reality Check
In startup land, “move fast and break things” isn’t just a motto – it’s survival. I’ve seen startups die not because their idea was bad, but because they spent six months building “scalable architecture” for a product that never found product-market fit. Here’s a decision framework I use:
The Innovation Killer
Best practices are, by definition, practices that worked well in the past. They’re optimized for known problems and established patterns. But what happens when you’re solving a problem that doesn’t fit those patterns? I once worked on a real-time data processing system where the “best practice” was to use message queues for everything. The problem? We needed sub-millisecond latency, and every abstraction layer added precious microseconds. The solution involved shared memory, lock-free data structures, and code that would make most senior developers weep. Did it violate every principle of clean code? Absolutely. Did it work perfectly for five years in production? You bet.
The Art of Selective Rebellion
Know Your Context
The key isn’t to abandon all practices – it’s to understand when they serve you and when they constrain you. Here’s my practical framework: Always ignore best practices when:
- Building throwaway prototypes
- Working under extreme time pressure with known technical debt acceptance
- Exploring completely new problem spaces
- Performance requirements trump maintainability Sometimes ignore best practices when:
- Working on small, isolated scripts
- Building one-time data migration tools
- Experimenting with new technologies
- Team expertise doesn’t match the practice complexity Never ignore best practices when:
- Building core business logic
- Working on security-critical components
- Multiple teams will maintain the code
- The cost of failure is high
The Technical Debt Ledger
When you choose to break rules, be intentional about it. Keep a technical debt ledger:
// TECHNICAL DEBT: Hardcoded configuration
// WHY: Rapid prototype for investor demo
// WHEN TO FIX: After Series A funding
// ESTIMATED EFFORT: 2 days
const API_ENDPOINTS = {
prod: "https://api.startup.com",
staging: "https://staging.api.startup.com"
};
// TODO: Replace with proper config management
const currentEnv = window.location.hostname.includes('staging') ? 'staging' : 'prod';
This approach acknowledges the shortcuts while planning for future improvements.
The One-File Rule
For small utilities and scripts, I follow the “one-file rule” – if it fits in one file and solves one problem, architectural purity is optional:
#!/bin/bash
# This script violates every shell scripting best practice
# It works perfectly for our deployment needs
DB_PASS="hardcoded_because_its_internal" # Sue me
SERVER_LIST="server1,server2,server3" # No external config needed
for server in ${SERVER_LIST//,/ }; do
sshpass -p $DB_PASS ssh deploy@$server "
cd /app &&
git pull &&
docker-compose restart api
"
done
echo "Deployment complete!" # No proper logging, fight me
Is this maintainable? No. Does it solve the immediate problem efficiently? Absolutely.
The Nuanced Approach
Reading the Room
The decision to follow or ignore practices often comes down to reading your environment: Team Maturity: A team of senior developers can handle more rule-breaking than junior developers who are still learning why the rules exist. Project Lifecycle: Early-stage projects benefit from flexibility; mature projects need stability. Business Context: A fintech app handling millions in transactions has different quality requirements than an internal tool used by five people.
The Explanation Principle
When you break practices, explain why:
class UserManager:
# ANTI-PATTERN ALERT: Mixing concerns
# Normally, we'd separate data access, business logic, and caching
# But for this microservice handling 50 requests/day, the overhead
# of proper separation outweighs the benefits
def __init__(self):
self.db_connection = sqlite3.connect('users.db')
self.cache = {}
self.email_client = SMTPClient()
def create_user_and_send_welcome(self, user_data):
# Yes, this method does three things
# Yes, it violates SRP
# No, I won't split it for 50 users/month
user_id = self.db_connection.execute(
"INSERT INTO users VALUES (?)", user_data
).lastrowid
self.cache[user_id] = user_data
self.email_client.send_welcome(user_data['email'])
return user_id
The Real World Perspective
Case Study: The Friday Deploy
Picture this: It’s Friday afternoon, production is down, customers are screaming, and you’ve identified the fix. It requires a small change that violates your team’s code review policy. Do you: A) Follow the process, wait until Monday for reviews, and let customers suffer all weekend B) Deploy the fix with proper documentation and address the process violation Monday If you chose A, you might be technically correct, but you’re practically useless. Sometimes being a professional means breaking your own rules to serve the larger purpose.
The Legacy Code Reality
Real-world codebases aren’t textbook examples. They’re living, breathing monstrosities that have evolved over years. Adding perfectly clean code to a messy system can actually make things worse by creating inconsistency. Sometimes the “best” practice is to match the existing style, even if it’s not ideal:
// Existing codebase uses this anti-pattern everywhere
public class LegacyProcessor {
public static String processData(String input) {
return GlobalUtils.transform(GlobalState.getData() + input);
}
}
// Your new code - should you fix everything or match the pattern?
public class NewProcessor {
// Option A: Follow best practices (creates inconsistency)
private final DataTransformer transformer;
private final DataProvider provider;
// Option B: Match existing pattern (maintains consistency)
public static String processNewData(String input) {
return GlobalUtils.transform(GlobalState.getData() + input);
}
}
Sometimes option B is actually better for the codebase as a whole.
Practical Guidelines for Rule Breaking
The 10-10-10 Rule
When considering breaking a practice, ask yourself:
- How will I feel about this decision in 10 minutes?
- How will the team feel about it in 10 months?
- How will the business feel about it in 10 years? If the answers are positive, neutral, and irrelevant respectively, you’re probably safe to break the rule.
Documentation Over Perfection
Instead of perfect code, aim for understandable decisions:
// CONTEXT: Black Friday traffic spike
// DECISION: Bypassing ORM for direct SQL
// TRADEOFF: Performance vs. maintainability
// REVIEW DATE: January 15th
const getHotDeals = async () => {
// Normally we'd use our ORM, but it adds 200ms overhead
// During Black Friday, every millisecond counts
const query = `
SELECT product_id, discount_percentage, stock_count
FROM deals
WHERE active = 1 AND stock_count > 0
ORDER BY discount_percentage DESC
LIMIT 50
`;
return await db.raw(query);
};
The Reversibility Test
Before breaking a practice, ask: “Can I easily reverse this decision later?” If the answer is yes, the risk is manageable. If no, proceed with extreme caution.
The Meta-Practice
Here’s the ultimate irony: having a framework for when to ignore best practices is itself a best practice. The real skill isn’t blindly following rules or rebelliously breaking them – it’s developing the judgment to know when each approach serves your goals. The best developers I know aren’t the ones who memorize the most patterns or follow the most practices. They’re the ones who understand the principles behind the practices and can adapt them to context.
Moving Forward
So, should you ignore best practices? Sometimes, yes. Should you do it thoughtfully, with clear reasoning and proper documentation? Always. The goal isn’t to write perfect code – it’s to write effective code that solves real problems for real people. Sometimes that means breaking rules. Sometimes it means following them religiously. The wisdom is in knowing the difference. Remember: best practices are tools, not commandments. Use them when they serve you, adapt them when they constrain you, and ignore them when they actively harm your goals. Your future self (and your teammates) will thank you for the pragmatism. And if anyone gives you grief about that global variable in your prototype? Tell them I sent you. What’s your most successful rule-breaking moment in development? Share your war stories in the comments – let’s normalize the reality that sometimes the “wrong” way is exactly right.