Your team is shipping pull requests twice as fast as last year, the CI pipeline is green, and every diff looks cleanly formatted. Yet your production incident rate is quietly climbing.
According to Google Cloud's DORA State of AI-Assisted Software Development research, while roughly 90% of engineering teams now use AI tools daily to accelerate coding, increased AI adoption without matching quality guardrails correlates directly with a measurable drop in software delivery stability. Code generation is no longer the bottleneck—code verification is.
When pull request volume doubles while human review capacity stays flat, engineering teams fall into the "LGTM trap": rubber-stamping large, plausible-looking diffs that harbor silent, state-corrupting bugs.
The Problem: Plausible Code Is Harder to Debug Than Broken Code
When a junior developer writes a bug from scratch, the code usually looks suspicious. Naming is inconsistent, edge cases are visibly missing, or the syntax feels clunky. A senior reviewer spots the smell in seconds.
AI-generated code flips this heuristic on its head. Modern coding assistants produce code that is:
- Syntactically idiomatic and cleanly formatted.
- Accompanied by confident, well-structured PR descriptions.
- Paired with unit tests that pass—because the tests were generated from the exact same flawed implementation context.
The Common Mistake: Local Correctness vs. System Invariants
Coding agents excel at local context (the open file and immediate type definitions), but they frequently miss system-wide invariants—such as idempotency, transactional boundaries, permission scoping, and partial failure recovery.
Consider a real-world backend scenario: updating a user's subscription tier and syncing billing credits.
What Gets Approved in a Hurry (The Silent Bug)
An engineer prompts an assistant to "Add an endpoint handler that upgrades a user's plan and grants bonus credits, with unit tests." The PR arrives looking clean:
// ❌ Looks clean in a 600-line PR diff, but breaks production invariants
export async function upgradeSubscription(
userId: string,
newPlanId: string
): Promise<{ status: string; credits: number }> {
const user = await db.users.findUnique({ where: { id: userId } });
if (!user) {
throw new Error("User not found");
}
// 1. External call inside business flow without idempotency key
await billingClient.chargeUpgradeFee(user.stripeCustomerId, newPlanId);
// 2. Non-atomic read-modify-write (race condition under concurrent requests)
const updatedCredits = user.credits + 500;
// 3. Separate DB writes without a transaction
await db.subscriptions.update({
where: { userId },
data: { planId: newPlanId, updatedAt: new Date() },
});
await db.users.update({
where: { id: userId },
data: { credits: updatedCredits },
});
return { status: "upgraded", credits: updatedCredits };
}
Why did this pass review?
- The TypeScript types compile cleanly.
- The auto-generated unit test mocked
billingClientanddb, asserting thatcreditsequaleduser.credits + 500. - It was buried inside a 14-file PR alongside boilerplate route definitions and DTOs.
The Consequence in Production
Two weeks later, a client network timeout triggers an automatic HTTP retry:
-
Double charging:
billingClient.chargeUpgradeFeeruns twice because no idempotency key was passed. -
Lost updates / Race condition: Concurrent requests read the same
user.creditssnapshot in memory before writing. -
Partial state corruption: If
db.users.updatefails due to a transient lock timeout, the user is billed and their plan is updated, but their credits vanish.
The Improved Approach: Designing for Verification, Not Just Generation
Fixing the code requires enforcing transactional boundaries, atomic increments, and idempotency—things a reviewer only catches when diffs are small and architectural constraints are explicit:
// âś… Production-safe: Idempotent, atomic, and transactionally bounded
export async function upgradeSubscription(
userId: string,
newPlanId: string,
idempotencyKey: string
): Promise<{ status: string; credits: number }> {
return await db.$transaction(async (tx) => {
// 1. Prevent duplicate execution via idempotency record lock
const existingOp = await tx.idempotencyKeys.findUnique({
where: { key: idempotencyKey },
});
if (existingOp) {
return existingOp.responsePayload as { status: string; credits: number };
}
const user = await tx.users.findUniqueOrThrow({ where: { id: userId } });
// 2. Pass deterministic idempotency key to external provider
await billingClient.chargeUpgradeFee(user.stripeCustomerId, newPlanId, {
idempotencyKey,
});
await tx.subscriptions.update({
where: { userId },
data: { planId: newPlanId, updatedAt: new Date() },
});
// 3. Atomic database increment instead of in-memory calculation
const updatedUser = await tx.users.update({
where: { id: userId },
data: { credits: { increment: 500 } },
});
const result = { status: "upgraded", credits: updatedUser.credits };
await tx.idempotencyKeys.create({
data: { key: idempotencyKey, responsePayload: result },
});
return result;
});
}
If the implementation forgets to wrap two database calls in a transaction, the generated test simply mocks both calls and asserts each was called once. To break this loop:Click to view the hidden test smell that masks AI bugs
When an AI assistant writes both the implementation and the test suite in the same prompt, it generates mirror tests—tests that assert what the code does rather than what the business contract requires.
Shifting the Workflow: How High-Performing Teams Adapt
As highlighted in Google's engineering practices on The Standard of Code Review, the primary purpose of code review is ensuring that the overall code health of the system improves over time. When code generation becomes virtually free, your review standards must tighten, not loosen.
Use this checklist before opening or approving any AI-assisted pull request:
- [ ] Cap PR size at 250 lines of logic: If AI helped you write 800 lines in 20 minutes, split it into three stacked PRs so a human can actually reason about the state transitions.
- [ ] Separate boilerplate from business logic: Never mix auto-generated migrations, DTOs, or formatting changes in the same PR as core domain logic.
- [ ] Audit the tests before the implementation: Check whether the tests verify edge cases and failure rollbacks, or if they merely assert that mocked functions were called.
- [ ] Check system boundaries first: Look directly at external I/O, database transactions, concurrency locks, and authorization checks—where local context models fail most often.
- [ ] Require the author to explain the failure mode: Every PR description should answer one human-written question: "What happens if this code fails halfway through execution?"
Explore more production open-source projects on my GitHub
The Takeaway
AI coding assistants don't create bad engineering cultures—they amplify your existing bottlenecks. If your team treats pull request velocity as the primary metric of productivity, AI will help you accumulate technical debt and silent production bugs at record speed. Keep batch sizes small, separate test design from implementation generation, and treat human code review as an architectural safeguard rather than a syntax check.
What is the most subtle bug or production failure mode that slipped through an AI-assisted PR in your team recently, and what guardrail did you add to catch it next time?
Sources & Further Reading
- Announcing the 2025 DORA Report: State of AI-Assisted Software Development – Google Cloud Blog
- The Standard of Code Review – Google Engineering Practices Documentation
- Your Team Ships 2x More Pull Requests Since Adopting AI (GitClear Research Analysis) – AlterSquare
Connect with Me
If you found this article helpful, let's connect!
- đź’» GitHub: johnnylemonny
Top comments (1)
And then there are security implications. See my today’s post