Vibe Coding is Cooling Off, Fast
Vibe Coding is Cooling Off, Fast
AI is great for PoCs, but fails at creating maintainable software

The title might sound a bit like clickbait, but even the guy who coined the term “vibe coding,” Andrej Karpathy, just pointed out that he hand-wrote his latest project. Whatever his reasons, the bottom line is: vibe coding just wasn’t good enough.
- Feb 3, 2025: The term “Vibe coding” is born.
- Oct 13, 2025: Andrej Karpathy gives an update on vibe coding.
That’s barely half a year. What the hell happened?
We were just celebrating how insanely capable Claude 4.5 and GPT-5 are, and suddenly the script flips?
Honestly, I’ve also been hitting the hard limits of vibe coding myself lately.
Let me provide a real-world example.
Let’s say we have a huge project and we need to introduce a behavior change. Whether it’s for A/B testing or feature toggling, we’re essentially just wrapping the old code in an if-else block to introduce the new behavior, right?
That’s a bit abstract, so let’s look at some pseudo-code.
1 | if is_enabled("feature A"): |
Super easy to understand, right?
Now, when this feature is big enough, these if-else blocks are going to be scattered all over the codebase. What do we think an AI Agent will do?
Yes. Exactly. It’s going to slap an if-else on every single place that needs to change. So, what happens when the feature toggle rollout is complete and it’s time to clean up the codebase?
Right again. The Agent will just go back and rip out all those if-else statements. What’s the problem with that? If that if-else is spread across 100 different files, this “housekeeping” task is now touching 100 files. Would we feel safe pushing that change to production?
So, what’s the right way to do it?
I mentioned this in a previous article on feature toggling: the correct practice is to introduce the Factory Pattern.
The beauty of this is that after the code is refactored, the actual feature flag check is hidden inside the factory. When it’s time for cleanup, we only have to modify the factory. We don’t have to touch all the integration points.
Let’s look at a concrete example.
The Factory Pattern in Practice
Let’s say we have some business logic for getting user data. The old implementation queries the database directly. The new implementation uses a dedicated microservice. We need to migrate all the database queries to the new REST API.
1 | def get_user_profile(customer_id: str, email: str): |
If we also have functions to get group info by user_id or other queries, we can imagine this if-else getting scattered everywhere.
This is where we bring in the Factory Pattern. It has three core components: an Interface, Concrete Implementations, and a Factory.
First, we define an abstract UserProvider interface. It defines the methods that all “user data providers” must have. This way, the calling code doesn’t need to care if the data is coming from a database or an API.
1 | from abc import ABC, abstractmethod |
Next, we create concrete classes for the old and new services, both of which implement the UserProvider interface.
The old database service:
1 | class OldDatabaseUserProvider(UserProvider): |
The new API service:
1 | class NewAPIUserProvider(UserProvider): |
Finally, we create a UserProviderFactory. Its only job is to decide which concrete UserProvider instance to return based on the given conditions (here, the customer_id and his feature flag).
1 | class UserProviderFactory: |
Now, we’ve completely encapsulated the object creation and selection logic within the factory.
After implementing the factory, the business logic function that was full of if-else statements becomes way simpler.
The client code no longer needs to care about “which service to use” or “how to build the service object.” It just asks the factory for a usable UserProvider.
1 | user_provider_factory = UserProviderFactory() |
In the future, when it’s time to clean up, we only need to touch the feature_flags.is_enabled line in the factory and delete the legacy OldDatabaseUserProvider. This ensures all client code is unaffected and dramatically shrinks the cleanup’s blast radius.
Here’s Another Thing the Agent Won’t Tell Us
Let’s do one more example.
Today’s AI agents are smart. They even write tests for their code. But if we actually look at the tests they write, we’ll find they are full of duplicated code, especially when they’re trying to cover a bunch of corner cases.
These kinds of unit tests are basically unmaintainable. A future logic change could instantly break a ton of test cases.
What’s the right way to do it? As I’ve explained in a previous article, we need to use table-driven testing to consolidate all the test cases.
A unit test follows the Arrange-Act-Assert (AAA) pattern. For different test cases, the only parts that really change are ‘Arrange’ (the inputs) and ‘Assert’ (the expected outputs). So, we can just build a table (an array) of all the cases and run them all through a single loop.
In fact, a lot of testing frameworks have-built in support for this. For example, pytest does this with mark.parametrize.
1 | import pytest |
The agent won’t tell us any of this.
Not unless we explicitly tell it: “I want us to implement the Factory Pattern” or “I want us to use table-driven testing.”
And this is the limit of vibe coding. I’ve spent a lot of time in the past discussing the practical details of vibe coding, and I was an early adopter of spec-driven development workflows.
But when I seriously review the AI’s output, I find that even with something as powerful as Claude 4.5, the code it produces is still full of redundancy. This redundant code might be correct, and it might even work, but over time, it will become completely unmaintainable.
Both humans and AI have a limited context window.
Ultimately, software development still comes back to good software engineering practices.
We need to discuss design patterns. We need to consider feature onboarding and all the other real-world architecture problems. Vibe coding is great for building a PoC (Proof of Concept) really fast. But a PoC is not an MVP. The moment we need to turn that PoC into a real product, we still have to rely on the experience and wisdom of a human software engineer to solve those real-world problems.
Wrap Up
We used two concrete examples to explain the limits of vibe coding: one with design patterns and one with table-driven testing.
We might be thinking, “Well, can’t we just define all these rules in the system instructions?” That’s… yes and no.
There are so many design patterns. If we don’t specify which one, can the AI really pick the right one? What we call “design patterns” are split into three major categories — Creational, Structural, and Behavioral — each with dozens of different patterns.
Remember, the AI has a limited context. In a massive codebase, it needs to identify the problem, break it down, and propose the right solution. I don’t need to tell us how hard that is.
And that’s just talking about the narrow definition of design patterns. That’s not even getting into Pattern-Oriented Software Architecture or all the higher-level architectural problems like microservices, event-driven architecture, etc.
These are not problems an AI can solve just by glancing at a codebase or reading a detailed spec.
Don’t get me wrong, I’m still going to use vibe coding. But for me, its value right now has been reduced to just a super-powered auto-complete.
To put it bluntly, the auto-complete that AI gives me is just a lot more flexible than the old-school, rule-based completion. But at the end of the day, that’s all it is.
A message from our Founder
Hey, Sunil here. I wanted to take a moment to thank you for reading until the end and for being a part of this community.
Did you know that our team run these publications as a volunteer effort to over 3.5m monthly readers? We don’t receive any funding, we do this to support the community. ❤️
If you want to show some love, please take a moment to follow me on LinkedIn, TikTok, Instagram. You can also subscribe to our weekly newsletter.
And before you go, don’t forget to clap and follow the writer️!
Originally published on Medium