Beyond Vibe Coding

Scroll to explore Scroll to explore

Matthew Atkinson

NOV 15, 2025

The Middle Ground

Earlier this year, Andrej Karpathy coined the term “vibe coding” to describe a new way of building software with AI: “fully give in to the vibes, embrace exponentials, and forget that the code even exists.” Merriam-Webster’s definition is blunter: “writing computer code in a somewhat careless fashion, with AI assistance.”

The idea is simple. You describe what you want, the AI writes the code, you run it. If it breaks, you paste the error back and ask for a fix. You do not read the code, you do not really understand the system, you just iterate until it appears to work. For throwaway weekend projects or quick prototypes, that can be fine.

But there is a catch. As Simon Willison put it: “Vibe coding your way to a production codebase is clearly risky. Most of the work we do as software engineers involves evolving existing systems, where the quality and understandability of the underlying code is crucial.”

I have some background in statistics and data work, and was comfortable with Python and SQL before starting on this LLM-assisted development journey. The things I build are not disposable experiments. They are tools I use to run my business and tools my clients rely on. Some are internal. Some have small user bases. Some are hosted in GCP with authentication, structured environments, and basic operational safeguards. They need to keep working, and when they do not, I need to be able to diagnose the problem, fix it, and move forward without rebuilding the entire system from scratch.

So I have landed somewhere in the middle. I use AI heavily in implementation, but not as a substitute for understanding. I still read most of the code (though and less as models improve). I still organize the project. I still make sure I understand how the system works before I rely on it.

So I've landed somewhere in the middle. I use AI to generate nearly all of the code, but I don't "forget that the code exists." I read it. I organize it. I make sure I understand what's happening and where—not because I want to, but because I've learned what happens when I don't. When something breaks in a vibe-coded mess, you're not debugging—you're first trying to figure out how the thing even works. Same story when you want to add a feature. With an organized project, you can find the problem, fix it, and move on. You can extend it without starting over.

 

The Workflow

Goals and Objectives

This sounds obvious, but it's important to actually write down what I'm trying to accomplish. What does success look like? What are the specific objectives I need to hit to get there?

Form follows function, and if I can't clearly articulate what I'm building and why, the AI can't either. And maybe just as importantly, I'll waste hours going in circles before realizing I never defined what "done" meant.

Product Requirements Document (PRD)

With goals and objectives written out, I ask multiple LLMs to create a product requirements document. Claude, GPT, Gemini—whatever I'm using at the time. I give them all the same prompt.

Then I compare the outputs. I'm not just picking the best one. I'm looking for where they diverge. If one mentions authentication and another doesn't, that's a flag. If one suggests a database and another suggests flat files, that's something I need to understand. The divergences reveal gaps in my own thinking—concepts I don't fully grasp, decisions I hadn't considered.

I quiz the models on the things I don't understand. Why did you suggest this? What's the tradeoff here? Is this actually necessary for what I'm trying to do? By the time I'm done, I understand the problem space well enough to make informed decisions about the approach.

Plan

Once I'm comfortable with the PRD, I give it to the models and ask them to generate an implementation plan. Same process—multiple models, compare outputs, dig into the differences.

I've found that getting one thing wrong in the plan can completely derail a project. So I spend time here.

Setup

Before I hand the model my plan and say execute, I set up the project structure: folders, .env file with API keys, credentials, git initialized, virtual environment. The point is that organization starts before the first line of code gets written.

Execution

Now I give the model the plan and tell it to execute. But I don't hand over everything at once. Sometimes the model needs to see the whole plan to understand how pieces connect. Other times, I deliberately withhold context so it doesn't get distracted or over-engineer a simple component. Either way, it's building one key component at a time. Build, verify it works, move on.

Tests

Testing has become one of the biggest upgrades in my workflow.

Earlier on, I treated tests as something useful but secondary. Mostly I’d tell the model to make sure it creates tests. Now for me they do several things at once. They force clearer thinking about expected behavior. They make it easier to validate changes without manually checking every path. And they give a feedback loop when a change introduces a problem.

That said, model-generated tests are not automatically trustworthy. I've noticed a failure mode. Sometimes when a test fails, the AI will modify the test to pass rather than fix the actual code. It's "cheating" in a way that's easy to miss if you're not paying attention.

 

A Note on Learning

You can skip the PRD and planning steps. Just give the AI your goals and tell it to build. Sometimes that works. But I've found the investment in actually learning—understanding what the models suggest and why—pays off. It lets me tackle larger, more complex projects. It speeds up future work because I'm not relearning the same concepts. For things you expect to build on or revisit, the upfront time is worth it.