本文以英文发布。
30 Days of Hands-Off AI Coding: Five Architectural Traps in a Real SaaS
A month of letting AI write all application code for a production micro-SaaS exposed drift, bad webhook handling, debug loops, bloat and a junior-developer problem.
Late 2026 brought the usual shouting: software engineering is dead, AI writes all the code now, plain language is the only syntax you need. To see where the hype stops and production begins, a 30-day experiment set one strict rule: no application code written by hand. AI wasn't a typing aid or autocomplete here. The human acted as architectural supervisor and reviewer, defining schemas, writing requirements, reading diffs and approving changes, while the model wrote the logic, queries and integrations.
The project wasn't a todo list. It was a real micro-SaaS: login, migrations, user dashboards, Stripe webhooks and public APIs with rate limits. It works, it's deployed and it serves live traffic. The first 40% of the build felt like wizardry. Then the repository passed 15,000 lines, distributed state arrived, and the "AI writes everything" idea hit hard engineering limits. Seven failure modes were logged. Five of them follow, along with the lessons they teach.
The early days really are fast
Fairness first: scaffolding a new greenfield project is close to a solved problem.
- A modern full-stack setup with Next.js, Tailwind, PostgreSQL, Drizzle ORM, auth middleware and Docker configs took an afternoon.
- Repetitive Zod schemas, validation helpers, sortable tables and pagination came out effortlessly, and predictable CRUD endpoints arrived clean, without a single typo.
- When one isolated query had a poor execution plan, the model quickly spotted an unindexed foreign key and proposed the exact index migration.
For six days, it seemed that traditional development might be obsolete. Then the second week started.
Trap 1: local focus, global drift
The most insidious weakness is local myopia. A model judges the file or function in its context window with remarkable precision, yet it can't see conventions set three directories away.
Architectural Drift Anti-Pattern
┌─────────────────────────────────────────────────────────────┐
│ Core Utilities (/src/lib/utils/format.ts): │
│ └── export const formatCurrency = (cents: number) => ... │
└─────────────────────────────────────────────────────────────┘
▲
(The AI ignores this file)
│
┌─────────────────────────────────────────────────────────────┐
│ New Billing Module (/src/modules/billing/invoice.ts): │
│ └── export const formatPriceUSD = (amount: number) => ... │
│ (Re-implements logic with different rounding edge cases)│
└─────────────────────────────────────────────────────────────┘
Within two weeks, the agent had written three slightly different currency-formatting helpers in separate modules, re-implemented an inline session check inside a new route handler instead of using the existing middleware pipeline, and declared two conflicting UserProfile interfaces in different packages that shared 90% of their fields but disagreed on date serialization.
None of that raised a compiler error, and the test runners stayed green. But the codebase was piling up silent architectural drift, the structural rot that can turn an agile project into an unmaintainable legacy mess in about six months.
Trap 2: plausible code that's wrong
The most dangerous AI-written bugs aren't crashes or syntax errors, because compilers catch the first and linters the second. The scary ones read well, run smoothly in local tests and quietly break transactional guarantees. Here is the actual pattern the AI produced for a Stripe billing webhook:
// The AI's Initial Webhook Handler (Dangerous Anti-Pattern)
export async function handleStripeWebhook(req: Request) {
const event = await parseStripeEvent(req);
if (event.type === 'checkout.session.completed') {
// 1. Immediately acknowledge event to Stripe
sendImmediate200Response(); // 2. Perform async database update in unhandled background promise
db.users.update({
where: { customerId: event.data.object.customer },
data: { isSubscribed: true }
}).catch(err => logError(err));
}
}
On a developer machine with mock data, this passes every test. It acknowledges the event right away, returns 200 OK and updates the local SQLite database. In production under load, it's a disaster waiting to happen:
- If the database hits a transient connection timeout or a deadlock inside that unawaited background promise, the error vanishes.
- The customer's card is charged and Stripe records the webhook as delivered, yet the account is never upgraded.
- Because a
200went back, Stripe never retries, so the transaction is lost for good.
Anyone who has debugged payment race conditions in production spots it instantly: webhooks have to be idempotent, processing has to finish inside an atomic transaction, and 200 OK may only be returned once the state is safely persisted. A junior engineer copying the neat-looking diff would have approved it without a second thought.
Trap 3: the endless self-debugging loop
Whenever a bug depended on state touched outside the file in view, telling the AI to fix its own output kicked off a maddening cycle:
The Debugging Despair Loop
┌────────────────────────────────────────────────────────┐
│ 1. Bug Identified: State race condition in worker │
└──────────────────────────┬─────────────────────────────┘
│
▼
[Prompt AI to Fix] ──► Confidently rewrites worker.ts
│
▼
[Run Tests] ─────────► Bug disappears; introduces memory leak
│
▼
[Point out Leak] ────► Confidently rewrites worker.ts again
│
▼
[Run Tests] ─────────► Re-introduces the original race condition!
The model had no runtime debugger to step through traces, inspect the heap or observe distributed timing, so it relied on probabilistic guesses drawn from error strings. It would cheerfully declare the bug fixed, show a clean diff and then bring back the original failure two iterations later. Solving these problems meant stepping in manually with an interactive debugger to look at raw thread state.
Trap 4: no ability to say no
Good engineering depends as much on what you refuse to build as on what you ship. Asked to design a feature such as a user account settings screen, the model defaulted to unchecked expansion. It produced 14 configurable toggles, elaborate notification-preference matrices and nested tabs that no user asked for. Asked for error handling, it wrapped every internal function in generic try/catch blocks that swallowed the root exception and returned "Something went wrong," destroying observability.
A model has no product intuition and treats more code as more value. Keeping interfaces lean, pruning needless abstractions and preserving readable stack traces remain human skills.
Trap 5: the last 10% takes the time
Software obeys a harsh rule of thumb: roughly the first 80% of features eat 20% of the schedule, and the final 20% eats the other 80%. AI automates the first part entirely. Interface scaffolding, REST endpoints and standard forms took less than a week. The other three weeks went to messy real-world work:
- Network jitter and exponential backoff retries
- Idempotent database writes across distributed webhooks
- Recovery states for edge cases, such as a user hitting Refresh halfway through an OAuth callback
- Memory leaks in long-running background workers
The AI doesn't think about edge cases unless you know to tell it to look, and knowing that takes years of production scars.
The junior developer paradox
The experiment dismantled the simple claim that AI will replace programmers. What it automated was the entry-level work that used to go to junior engineers, not senior engineering.
The Knowledge Pipeline Dilemma
Traditional Career Path:
[Junior: Scaffolding / CRUD] ──► [Mid-Level: Debugging / APIs] ──► [Senior: System Design]
▲
Today's Reality: │
[AUTOMATED BY AI] ──► [AUTOMATED BY AI] ──► [Senior Judgment Required]
│
Question: Where do tomorrow's senior engineers build their intuition? ──┘
Boilerplate setup, unit test scaffolding, CSS grid layouts and plain CRUD queries are exactly where novices once built their mental models of software systems. You can't jump straight to distributed-systems architecture without hundreds of hours fighting syntax, chasing off-by-one errors and learning why badly designed schemas collapse under load. AI has raised the value of senior judgment: spotting code that's plausibly wrong, rejecting unnecessary abstractions and keeping the whole system architecture in your head to prevent drift. At the same time, it has removed the ladder that juniors used to climb toward that expertise.
What changed in the workflow
AI is still in use, including for most raw text generation, but the illusion that producing code is the same as engineering is gone. Three rules now apply:
- The author is never the reviewer. An agent that writes a diff shouldn't be the one to validate it. Run a separate, adversarial review agent prompted to find edge-case failures, or read every line yourself with a skeptic's mindset.
- Passing tests are only the starting line. Never accept a pull request just because the tests are green, since models are good at writing tests that confirm their own flawed assumptions.
- AI types, humans own the architecture. Let the machine produce repetitive syntax, but keep the data models, system boundaries, transactional guarantees and operational invariants under human direction.
The machine can write code faster than any person in history. Still, code is a liability, and the system it forms is the asset.