By now you’ve probably heard the term “vibe coding.” Coined by former OpenAI founding team member Andrej Karpathy in early 2025, the phrase describes a style of development where you “fully give in to the vibes, embrace exponentials, and forget that the code even exists.” In practice, it means building software primarily by describing your intentions to an LLM instead of carefully planning specs and architecture.
The results can feel magical. They can also hide serious problems. Karpathy’s own tweet even described some of the problems that most seem to have ignored:
There’s a new kind of coding I call “vibe coding”, where you fully give in to the vibes, embrace exponentials, and forget that the code even exists. It’s possible because the LLMs (e.g. Cursor Composer w Sonnet) are getting too good. Also I just talk to Composer with SuperWhisper so I barely even touch the keyboard. I ask for the dumbest things like “decrease the padding on the sidebar by half” because I’m too lazy to find it. I “Accept All” always, I don’t read the diffs anymore. When I get error messages I just copy paste them in with no comment, usually that fixes it. The code grows beyond my usual comprehension, I’d have to really read through it for a while. Sometimes the LLMs can’t fix a bug so I just work around it or ask for random changes until it goes away. It’s not too bad for throwaway weekend projects, but still quite amusing. I’m building a project or webapp, but it’s not really coding - I just see stuff, say stuff, run stuff, and copy paste stuff, and it mostly works.
The Seduction of Vibe Coding
You ask, the model creates. It’s hard not to get hooked. I recently revisited my very first iOS app, a winter weather forecast app I built in 2012 using Objective-C. It had long since been removed from the App Store due to neglect, and the project had reached a point where Xcode could no longer build it. The biggest issue I found is that the source code for a particular graph view had gone missing. I had known about this for a couple years and considered whether I could reverse engineer it based on its API and how it was used. I knew it was possible, but the task was daunting and likely to take considerable time.
I decided to turn to Claude Code. I described the issue in a couple sentences, and after just a few minutes, Claude had rebuilt the missing file and simultaneously breathed life into an app that had been dead for a decade.
That experience captures the intoxicating appeal of vibe coding perfectly. Problems that once required hours of searching through Stack Overflow posts, Apple documentation, and old commit histories can now be solved in minutes just by asking. Entire categories of friction in software development suddenly disappear. The effect is especially dramatic for experienced developers, because you can feel the leverage immediately.
In a way, it’s like playing a slot machine. You pull the lever, lights flash, bells ring out, and you wait a few moments hoping to hit the jackpot. Then the model spits out hundreds of lines of code, the app compiles, your feature works, and your brain gets the same little dopamine rush as if you were watching coins pour out of a slot machine. You feel incredibly productive, powerful even. But like a jackpot in Vegas, the excitement can obscure the looming tax bill that comes due later.
Lost in Translation
Human language is inherently ambiguous. During the process of bringing my weather app back to life I decided I wanted to bring it up to modern standards by converting it from Objective-C and UIKit to Swift and SwiftUI. I wasn’t asking for any new features or changes, just a 1-to-1 port. So I prompted the model with a seemingly simple, straightforward request: Rebuild this project in Swift and SwiftUI. Sure enough, 12 minutes later I had a fully working Swift/SwiftUI version of my app.
But — much to my surprise — it wasn’t exactly what I thought I had asked for. The model hadn’t just done a 1-to-1 port. It had reimagined the entire app from scratch with an entirely new user experience. I hadn’t specified that I didn’t want changes beyond the language update, so the LLM took it on itself to make the app look and feel more modern.
At first, it felt miraculous. The redesigned experience was honestly better than the original app I had built years earlier. But the deeper I dug into the implementation, the more cracks appeared. Important functionality was missing. Existing forecast parsing logic had been discarded. Some views were rendering incorrect data. The app looked complete long before it actually was.
Details Matter
Engineering involves caring about and understanding the details of how a thing works. In a production system, the choices we make can have a huge impact on our app’s performance or security. How do we structure our data? What database do we choose for local storage? How do we architect our API interaction layers? Without understanding these details, the choices you make could result in your app’s performance being greatly degraded or its security compromised.
As an example, consider an app related to finance and real-time trading analysis. This is a scenario where we need high throughput database writes and guaranteed data integrity. An engineer intuitively knows and understands this, but a model may choose something that works but will never scale. A database choice that works perfectly during development with a few hundred records may completely collapse once thousands of real-time updates begin competing for writes.
An experienced engineer develops intuition around these tradeoffs because they’ve lived through the consequences of getting them wrong. They know SQLite with proper indexing and transactional batching may outperform a more fashionable abstraction layer in a write-heavy environment. They understand why eventual consistency may be acceptable for social media likes but unacceptable for financial transactions. They recognize that an ORM convenience feature generating dozens of hidden queries can quietly hinder performance at scale.
In software engineering, maintaining systems over time is often more challenging than building them. Production software accumulates edge cases, legacy integrations, and historical decisions that future developers must understand. Code generated through pure vibe coding can quickly become difficult to reason about, because no developer ever formed a strong mental model of how the system actually works. Debugging becomes slower, refactors become riskier, and eventually the codebase turns into something the developer is afraid to touch.
This is where the distinction between coding and engineering becomes impossible to ignore. The hardest part of building quality software is understanding the invisible constraints beneath the surface: scalability, fault tolerance, latency, synchronization, security, maintainability, operational cost, and long-term evolution. Details matter because production systems eventually expose every shortcut.
Security Isn’t Automatic
Ask a model to integrate a third-party API into a mobile app, and there’s a decent chance it will place the API key directly inside the client application bundle. The feature works. The demo succeeds. But the security model is fundamentally broken because the model is optimized for functionality, not operational safety.
This is a recurring pattern with AI-generated software. The model will often choose the shortest path to a working implementation, even when the path violates basic security principles. Authentication tokens end up stored insecurely. Database rules become overly permissive to avoid access errors.
The problem is deeper than isolated mistakes. Security engineering requires an adversarial perspective. It requires thinking not just about a software’s intended use but also about how it could be misused or even attacked. A secure system must survive both unexpected input and various intentional bad actions. Current AI models won’t make these considerations unless guided by a knowledgeable engineer who understands the risks.
Even more dangerously, insecure code is often indistinguishable from secure code to an inexperienced developer: It’s easy to assume that when the model outputs working code, it’s secure by default — but this is often not the case. What may appear to be production-ready on the surface can hide severe vulnerabilities underneath.
Finding A Middle Ground
Vibe coding vs. engineering is not a binary choice — it’s more like a slider. Engineering takes time, research, and careful consideration. Every time you prompt the model, you’re figuring out how to adjust that slider. In some instances, you may not want to spend the time nailing down all the engineering answers, and it can be helpful to let the model fill in the blanks and see where you land.
There’s also incredible value in iteration. Successful AI-assisted development doesn’t necessarily begin with a fully engineered specification — it begins with exploration. You start with a rough idea, see what comes back, and then start tightening the screws. The first prompt might be pure vibe coding: “Build me a real-time stock screener app with live charts and alerts.” The result may be messy, insecure, or architecturally questionable, but it gives you something incredibly valuable: a concrete starting point. Once you have that, engineering can begin.
From there, the workflow shifts. You stop asking the model to simply “build the thing” and instead guide it through increasingly specific engineering concerns. Replace polling with WebSockets. Move API keys server-side. Refactor the persistence layer to support high-frequency writes. Add retry handling and offline support.
You begin iterating not just on features but on correctness, performance, and security.
The Future Still Needs Engineers
Vibe coding is an incredibly powerful concept, but it is not a replacement for engineering. LLMs act as levers, enabling developers to accomplish more in less time.
In many ways, this is simply the next evolution of abstraction in software development. Early programmers worked directly in assembly and machine code, a painstaking process. Higher-level programming languages changed that. A compiler became a translation layer, converting human-readable intent into machine-executable instructions. Productivity exploded, but the need for engineering did not disappear. We still needed people who understood systems, architecture, performance, tradeoffs, and failure modes. LLMs represent another layer in that same progression. Instead of translating Swift or Python into machine code, they translate natural language into Swift or Python.
The future likely belongs neither to traditional engineering nor vibe coding, but to developers who understand how to combine the two. The ability to move quickly matters. The ability to understand what you’ve built matters even more.