Feature image for blog post about software principles for vibe coders

5 Software Principles Vibe Coders Need

The 2000s called. They want their wisdom back.

10 min read
Share:

You're shipping features with Claude, ChatGPT, or Cursor. You're building apps you couldn't have dreamed of six months ago. University lectures about "software engineering principles" sound about as relevant as COBOL tutorials.

Here's the truth I've discovered watching both AI-native developers and traditionally-trained mid-level engineers: those dusty principles from the '80s, '90s, and 2000s? They matter more now than ever.

Why?

When you're collaborating with an AI that has a context window measured in tokens, every line of code you write either helps or hurts. Those principles you ignored? They're actually context-reducing techniques that nobody told you about.

The Vibe Coding Paradox

I've been in this industry long enough to see three generations of developers:

  1. The University Crew (me included) - Sat through lectures at school or even our workplace, on SOLID, DRY, and design patterns, while secretly just wanting to ship code
  2. The Bootcamp Generation - Learned practical skills fast, picked up theory later (if at all)
  3. The Vibe Coders - Learning to code through AI pair programming, building real things from day one

Here's what's wild: the vibe coders are often more productive initially. But around month 3-6 of a project, I'm seeing the same patterns emerge that plagued us "educated" developers who ignored our professors.

The difference now? Your API context window doesn't care about your credentials. It cares about whether your code is understandable.

And that's where these principles come in.

The 6 Principles

1. DRY (Don't Repeat Yourself)

Or: Why Your API Bill is $500/Month

If you write the same logic twice, you've created two sources of truth. One will inevitably be wrong.

Why this matters now: Every time that authentication check gets repeated in five different files, the AI has to read, understand, and maintain five different versions. That's 5x the tokens, 5x the chance of bugs, and 5x the cognitive load when you ask Claude to "add rate limiting to my auth."

Here's a real example I've seen:

// ❌ Repeated in 8 different API routes
if (!req.headers.authorization) return res.status(401).json({error: "Unauthorized"})
const token = req.headers.authorization.split(" ")[1]
const user = await verifyToken(token)
if (!user) return res.status(401).json({error: "Invalid token"})

VS:

// ✅ the CODE, put in middleware!! Reusable across all routes
const requireAuth = async (req, res, next) => { /* auth logic */ }

// Now, when you need to change auth, it's ONE change

The context window win: The AI reads 10 lines of middleware + 8 route declarations instead of 50 lines of repeated auth code. Smaller context, smaller API bills, faster changes.

2. SOLID (Especially Single Responsibility)

Or: Why Claude Keeps Hallucinating Your Code

Each piece of code should do ONE thing. Your UserService shouldn't also handle email sending, payment processing, and PDF generation.

Why this matters now: When you ask an AI to "fix the user registration," and your UserController does 15 different things, the AI has to understand ALL 15 things to make ANY change. This is how you get hallucinations and broken code.

The smoking gun? If you're typing "Actually, ignore the email part, just fix the user creation" in your second prompt, you've already lost. Your code violated Single Responsibility.

Here's a better approach:

// ❌ God Class that does everything
class UserManager {
  createUser() { /* creates user, sends email, logs analytics, updates cache */ }
}

// ✅ Each class has ONE job
class UserRepository { createUser() { /* just database stuff */ } }
class EmailService { sendWelcome() { /* just email stuff */ } }
class UserRegistrationOrchestrator {
  register() {
    // coordinates the others, easy to understand - using calls like createUser(), sendWelcome() etc..
  }
}

The context window win: When debugging, the AI only needs to see UserRepository (50 lines) instead of UserManager (500 lines mixing concerns).

3. YAGNI (You Aren't Gonna Need It)

Or: Stop Building for a Future That Won't Exist

Don't build functionality until you actually need it. This one's nuanced though, because there's a difference between foundational architecture and feature bloat.

Why this matters now: This is where AI tools get dangerous. They make it easy to over-engineer. Ask Claude to "build a user settings page" and you might get:

  • Dark mode toggle
  • Email notification preferences
  • Language selector
  • Export data button
  • Delete account flow
  • Accessibility settings
  • Custom themes

When all you needed was a password change form.

The AI isn't pasting code - it's generating it. And it'll happily generate mountains of "helpful" features you didn't ask for.

The nuance: There's a difference between YAGNI for features vs. architecture.

A basic plugin system on day 1? That might save you weeks later. But don't ask the AI to build the whole plugin marketplace, documentation site, and versioning system today.

That settings page with 7 features you don't need? That's pure bloat filling your context window.

Better prompt: "Claude, build a settings page with ONLY password change. Nothing else."

The context window win: 50 lines of focused code the AI can understand beats 500 lines of "what if we need..." that clutters every future prompt.

4. Separation of Concerns

Or: Your UI Shouldn't Be in Your Database Layer

Business logic, data access, and presentation should be in different layers. Your React component shouldn't have SQL queries. Your database model shouldn't know about HTTP status codes.

Why this matters now: When everything is mixed together, you can't ask targeted questions. "Fix the display of user names" becomes a full codebase refactor because user name logic is EVERYWHERE.

Real conversation I overheard:

Developer: "Claude, update the user avatar to show their initials" Claude: Modifies 12 files including API routes, database migrations, and email templates Developer: "Wait, why did it change my emails?"

The fix is simple: Separate your concerns. UI components for display. Services for logic. Repositories for data. When they're separate, you can modify one without touching the others.

The context window win: Need to change how dates display? If date formatting is separated from business logic, the AI only needs to see the formatting utility. If it's mixed in with everything, the AI has to load your whole app into context.

5. Configuration Over Code

Or: Your Business Logic Shouldn't Be in If Statements

Instead of hardcoding behaviour in if/else chains or switch statements, drive behaviour through configuration. Your code reads the config, the config defines the behaviour.

Why this matters now: This is the secret weapon for context window optimisation that nobody talks about. When your business rules live in a config file, you can change behaviour WITHOUT changing code. And more importantly, the AI can read just the config instead of a 500-line function.

The anti-pattern I see everywhere:

// ❌ Business logic hardcoded - every change needs code changes
function getShippingCost(country, weight, method) {
  if (country === 'US') {
    if (weight < 5) {
      if (method === 'express') return 25
      if (method === 'standard') return 10
    } else if (weight < 20) {
      if (method === 'express') return 45
      if (method === 'standard') return 20
    }
    // ... 50 more lines of if statements
  } else if (country === 'UK') {
    // ... another 50 lines
  }
  // You get the idea. This is unmaintainable.
}

The configuration approach:

// ✅ Behaviour driven by config
const shippingRules = {
  US: {
    weightBands: [
      { max: 5, express: 25, standard: 10 },
      { max: 20, express: 45, standard: 20 }
    ]
  },
  UK: {
    weightBands: [
      { max: 5, express: 30, standard: 15 }
    ]
  }
}

function getShippingCost(country, weight, method) {
  const rules = shippingRules[country]
  const band = rules.weightBands.find(b => weight < b.max)
  return band[method]
}

Now when you need to update shipping costs:

  • ❌ Old way: "Claude, update all the hardcoded shipping prices in this 500-line function"
  • ✅ New way: "Claude, update the shipping config for UK to increase express by $5"

The Rails/Django crowd made "Convention over Configuration" famous, but Configuration over Code is its powerful sibling. Rails conventions (like file naming) reduce boilerplate. Configuration reduces hardcoding.

Real example from my consulting work - client had a feature flag system hardcoded:

if (user.email.includes('@bigclient.com') ||
    user.tier === 'enterprise' ||
    user.betaTester === true ||
    specialUserIds.includes(user.id)) {
  // show new feature
}

Every time sales wanted to add someone to the beta, they needed:

  1. A code change
  2. A PR review
  3. A deployment
  4. Fingers crossed

Switched to config:

const featureFlags = {
  newDashboard: {
    rules: [
      { type: 'emailDomain', value: 'bigclient.com' },
      { type: 'userTier', value: 'enterprise' },
      { type: 'betaTester', value: true }
    ]
  }
}

Now sales updates a JSON file. CI validates it. Auto-deploys. No developer needed.

The context window win: When debugging or modifying features, the AI reads the config (10 lines) instead of the implementation (100+ lines). It understands intent faster.

6. Composition Over Inheritance

Or: Stop Building Class Hierarchies Nobody Can Navigate

This one's a bit more technical, but it's worth knowing. Instead of building complex inheritance trees (User extends Person extends Entity extends Model), compose behaviour from smaller pieces. Think Lego blocks, not family trees.

Why this matters now: Modern React, Vue, and component-based thinking already teach composition. But I still see developers (especially those with OOP backgrounds) building inheritance nightmares that make AI tools completely lost.

The Plot Twist

Here's what gets me: these principles were always right. We just ignored them because we thought we understood our own clever code.

Spoiler: We didn't. We just hadn't tried to change it in 6 months yet.

The AI revolution didn't create the need for clean code. It just removed our ability to fool ourselves about whether our code is actually understandable.

The Uncomfortable Part

Hot take: If Claude can't understand your code, you probably don't either.

I know, I know. "But I wrote it! Of course I understand it!"

Try this: Take some code you wrote 3 months ago. Don't look at it in the meantime. Now try to add a feature.

How long did it take you to remember what it does? If the answer is more than 30 seconds, your code failed the clarity test. The AI is just making this obvious faster.

What This Means

If you're vibe coding and this all sounds like gatekeeping, hear me out:

  1. You're actually ahead - You're learning to write AI-readable code from day one. We had to unlearn our "clever" habits.
  2. These principles = cost savings - Smaller context windows = cheaper API calls = more budget for building
  3. They're force multipliers - Simple, well-separated code means Claude can help you faster
  4. They prevent the rewrite - Every project that violates these principles eventually gets rewritten. Every. Single. One.

What to Do About It

If you're coding with AI assistance (and who isn't these days?):

  1. Before asking AI to modify code: Can you explain what it does in one sentence? If no, refactor first.
  2. Before copy-pasting: Ask "could this be a function/component?" (DRY)
  3. Before adding "flexibility": Ask "do I need this TODAY?" (YAGNI)
  4. Before mixing concerns: Ask "if this changes, what else breaks?" (Separation)
  5. Before hardcoding: Ask "could this be config?" (Configuration over Code)

The Conclusion

The university professors who droned on about software engineering principles in 1995? They were right. They were just 30 years too early.

The bootcamp instructors who said "ship first, principles later"? They were right too. For solo projects with short lifespans.

But if you're building something that will exist in 6 months, might need to scale, could have other developers (human or AI), or needs to be maintainable - then yeah, you need these principles.

Not because some old guy said so. Because your context window, your API bill, and your future self will thank you.

I've seen a lot of code over 20 years. The principles above aren't new. But the context window constraint? That's making them essential again.

What's your take? Are you using these principles in your AI-assisted development? Or are you learning them the hard way?

Let me know - I'd love to hear your experiences.

P.S. There are two hard things in software engineering: naming things and off-by-one errors.