AI has changed how fast we can write code. A decent prompt can now give you a working script, a UI stub, a FastAPI route, or even a rough end-to-end feature in minutes.
That is genuinely useful. I use AI coding tools all the time. But I also think a lot of teams are starting to confuse generated code with engineered software, and those are not the same thing.
Vibe coding is not the problem. Treating vibe-coded output like production-ready engineering is the problem.
What I Mean by Vibe Coding
When I say vibe coding, I mean the style of working where you prompt an AI tool, accept most of what it gives you, run it, patch a couple of errors, and keep moving.
That works surprisingly well for:
- Prototypes
- Proof of concepts
- Learning a new library
- UI experiments
- Throwaway scripts
- Hackathons
- Brainstorming implementation options
I do this myself. If I need to test an idea quickly, I do not want to spend an hour polishing structure before I know the idea is even worth keeping.
Here is a totally reasonable example:
import pandas as pd
df = pd.read_csv("orders.csv")
summary = (
df.groupby("status")
.agg(order_count=("order_id", "count"),
total_amount=("amount", "sum"))
.reset_index()
)
print(summary)
For quick exploration, this is fine. It answers a question fast. It helps you inspect data. It may be exactly what you need for ten minutes of analysis.
But this is not engineering yet.
Where Vibe Coding Works Well
Vibe coding shines when the cost of failure is low and the main goal is speed.
A few examples:
- You want to sanity-check a CSV before loading it into a warehouse.
- You want to test a new SDK without reading the whole documentation.
- You need a quick internal demo for a product idea.
- You are exploring three different implementation patterns and want rough drafts first.
In those moments, AI is a force multiplier. It cuts blank-page time. It helps you learn by doing. It gives you something concrete to react to.
That is valuable. I just do not confuse that value with production readiness.
Where Engineering Begins
Engineering starts when the code has to be trusted.
Trusted by other developers. Trusted by downstream systems. Trusted by customers, auditors, operators, managers, and business processes that assume the software will behave correctly even when inputs are messy and real life is inconvenient.
That is the line I draw.
Once code crosses that line, the job changes. Now I care about:
- Clear requirements
- Edge cases
- Input validation
- Error handling
- Logging
- Testing
- Deployment
- Ownership
- Security
- Performance
- Long-term maintainability
This is where senior engineering judgment matters. The real value is not typing faster. It is knowing what should be built, what should not be built, what can fail, what needs monitoring, and what needs to be explained clearly to the team.
Same Example, But Engineered
Here is the same CSV example with a bit more engineering discipline:
import logging
from pathlib import Path
import pandas as pd
REQUIRED_COLUMNS = {"order_id", "status", "amount"}
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def load_orders(file_path: str) -> pd.DataFrame:
path = Path(file_path)
if not path.exists():
raise FileNotFoundError(f"Input file not found: {file_path}")
df = pd.read_csv(path)
missing_columns = REQUIRED_COLUMNS - set(df.columns)
if missing_columns:
raise ValueError(f"Missing required columns: {missing_columns}")
if df["amount"].isnull().any():
raise ValueError("Amount column contains null values")
return df
def summarize_orders(df: pd.DataFrame) -> pd.DataFrame:
return (
df.groupby("status")
.agg(
order_count=("order_id", "count"),
total_amount=("amount", "sum")
)
.reset_index()
)
def main() -> None:
try:
logger.info("Loading order data")
orders = load_orders("orders.csv")
logger.info("Generating order summary")
summary = summarize_orders(orders)
logger.info("Summary generated successfully")
print(summary)
except Exception as exc:
logger.exception("Failed to process order data")
raise exc
if __name__ == "__main__":
main()
The first version answers a question. The second version is starting to behave like software someone else can depend on.
It is still small, but it has shape. You can test summarize_orders() independently. You can fail early on bad input. You get logs. You have a place to extend behavior without turning the whole file into a fragile pile of assumptions.
That is the difference. Engineering is not just making code work. It is making code dependable.
The Line I Draw
Here is the simplest way I explain it:
| Area | Vibe Coding | Engineering |
|---|---|---|
| Goal | Make it work | Make it reliable |
| Speed | Very fast | Controlled speed |
| Validation | Manual testing | Automated tests/checks |
| Ownership | Prompt-driven | Design-driven |
| Failure handling | Often missing | Expected and handled |
| Security | Often overlooked | Designed in |
| Documentation | Minimal | Useful and intentional |
| Maintainability | Secondary | Core requirement |
If I am building a quick script for myself, I am comfortable staying on the left.
If the code will live beyond today, touch production data, support a customer workflow, or wake somebody up at 2 a.m., I need the right side of that table.
AI Agents Make This More Important
The line gets even more important with agentic coding tools.
An AI agent does not just suggest one function anymore. It can modify multiple files, add dependencies, generate tests, update configs, write migrations, and run commands. That is powerful, but it also means mistakes spread faster.
When an agent makes changes, I review at least these things:
- Generated diffs, especially across multiple files
- New dependencies and their security or maintenance cost
- Security-sensitive logic
- Hidden assumptions in config or environment handling
- Tests that only cover the happy path
The danger is not that the AI writes bad code every time. The danger is that it often writes code that looks complete before it is actually safe.
Example: AI-Generated API Endpoint
This is the kind of endpoint an AI tool can generate in seconds:
from fastapi import FastAPI
app = FastAPI()
@app.get("/customer/{customer_id}")
def get_customer(customer_id: int):
return {"customer_id": customer_id, "name": "Test Customer"}
For a mock demo, that is fine.
For a real system, I immediately start asking harder questions:
- Who is allowed to access this endpoint?
- Where does the data come from?
- What happens if the customer does not exist?
- Are requests logged safely?
- Are we leaking private information?
- Do we need rate limiting?
- How is this monitored in production?
- What is the validation story beyond type hints?
Instead of pretending the first draft is enough, I use a checklist:
- Authentication
- Authorization
- Data access layer
- Error handling
- Structured logging
- Request and response validation
- Rate limiting
- Privacy controls
- Monitoring and alerting
That checklist is engineering. The generated route is just a starting point.
Practical Rules I Follow
These are the rules I keep coming back to:
- I use AI to accelerate implementation, not replace thinking.
- I do not accept security-sensitive code without review.
- I ask AI to explain tradeoffs, not just generate code.
- I write or review tests myself.
- I treat generated code as a draft.
- I check dependencies before adding them.
- I verify edge cases.
- I do not let confident explanations hide real complexity.
The senior engineer advantage is not raw typing speed anymore. It is judgment under ambiguity.
Good Prompts Engineers Should Use
Most bad outcomes start with lazy prompts and even lazier review. Better prompts help, especially when they force the model to think about failure.
A few prompts I actually find useful:
Review this code for edge cases, security risks, and production-readiness issues.
What assumptions does this implementation make, and which of them are risky?
Generate unit tests for success, failure, boundary, and invalid input scenarios.
Refactor this code for readability, testability, and maintainability without changing behavior.
Explain what could go wrong if this code runs in production at scale.
These prompts do not remove the need for engineering judgment. They just make AI more useful to an engineer who is already thinking like one.
Closing
Vibe coding is a great starting point. I use it often, and I think engineers who ignore AI tools are making life harder than it needs to be.
But software becomes valuable when people can trust it, and trust does not come from a convincing code generation. It comes from engineering discipline.
AI can absolutely make good engineers faster. It does not remove the need for judgment, accountability, or careful design. If anything, it makes those things more important.
The future does not belong to the people who can prompt the fastest. It belongs to the engineers who can combine AI speed with production discipline.
Top comments (0)