5 Architecture Patterns AI Gets Wrong
Last month a Series A founder sent me their codebase with a single question: "Why does everything break when we get more than 200 users?"
I opened the repo. Cursor-generated Next.js app. 43 files. Every route handler contained business logic, database queries, validation, error handling, and response formatting — all in one function. The longest was 847 lines.
I didn't need to read past the first three files to diagnose the problem. I'd seen this exact architecture in the last 30 codebases that crossed my desk. AI coding tools produce the same five structural patterns, and every single one of them detonates under real-world conditions.
Here's what they are, why they happen, and what production code actually looks like.
Pattern 1: God Components
What AI generates: One component file that handles data fetching, state management, business logic, form validation, API calls, error handling, and rendering. A single React component that's 400-900 lines long with 15 useEffect hooks, 8 useState calls, and inline fetch requests scattered throughout the JSX.
I reviewed a dashboard component last week — built by Cursor — that contained the entire user management system. User list, user detail, user edit form, role assignment, activity log, and export functionality. All in one file called Dashboard.tsx. 712 lines. When the founder wanted to add a search feature, they prompted the AI and it added another 80 lines to the same component. When they wanted filters, another 60 lines. The file grew like a tumor.
Why it breaks: God components are untestable, unreviewable, and impossible to modify without side effects. This is a major contributor to vibe coding technical debt that compounds with every feature you add. Change the sort order on the user list and you accidentally break the edit form because they share state through a useState hook declared 300 lines above. Three developers can't work on the same feature simultaneously because it's one file. Performance degrades because every state change re-renders everything.
What production code looks like: Separation by concern. A container component that handles data fetching. Presentation components that only render. Custom hooks that encapsulate business logic. A service layer that manages API communication. Each piece is independently testable, independently deployable, and comprehensible in isolation. A dashboard feature might have 12-15 files instead of one — and that's a feature, not a bug.
Pattern 2: Missing Middleware and Service Layer
What AI generates: Business logic directly inside API route handlers. Need to validate input? It's in the route. Need to check permissions? Inline if-statement in the route. Need to send an email after creating a user? await sendEmail() right there in the POST handler, between the database insert and the response.
Here's what I see in every AI-generated API route:
POST /api/users → validate body → check auth → check permissions →
query database → transform data → send email → log event →
update analytics → return response
All in one function. No layers. No separation.
Why it breaks: When your payment webhook needs to execute the same "create subscription" logic that your API route uses, you copy-paste. Now you have two copies. They drift. One gets a bug fix, the other doesn't. You end up with six different places that "create a user" and none of them do exactly the same thing.
Authorization checks get inconsistent — one route checks permissions, another forgets. We audited an app where 14 out of 38 API routes had no auth middleware because the developer added auth to each route manually and missed more than a third of them. This is one of the security vulnerabilities AI-generated code creates almost by default.
What production code looks like: Three layers. Route handlers do exactly one thing: parse the request and call a service function. Service functions contain business logic, validation, and orchestration. Data access functions handle database queries. Middleware handles cross-cutting concerns like auth, rate limiting, and logging. When your webhook needs the same logic as your API route, they both call the same service function. One source of truth. One place to fix bugs.
Pattern 3: Client-Side State for Server-Side Data
What AI generates: useState and useEffect for everything. User profile? useState. Product list? useState. Shopping cart that persists across sessions? useState. Dashboard data that 50 users view simultaneously? You guessed it — useState with a useEffect that fetches on mount.
The pattern looks like this in every AI-generated codebase:
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false))
}, [])
Repeated in every component that needs data. No caching. No deduplication. No stale-while-revalidate. No optimistic updates.
Why it breaks: Open your Network tab on a typical AI-generated app. You'll see the same endpoint called 3-7 times on a single page load because multiple components independently fetch the same data. Navigate away and back — every request fires again. The app feels sluggish because every page transition starts with a loading spinner while data that was fetched 10 seconds ago gets fetched again.
Worse: there's no single source of truth. Component A fetches user data and caches it in local state. Component B fetches the same user data. User updates their profile through Component A. Component B still shows stale data until the user refreshes the page.
In a Next.js App Router codebase — which is what most of these AI-generated apps use — this pattern actively fights the framework. Server Components fetch data at the server level, eliminating client-side loading states entirely. But AI tools default to the React patterns they were trained on, which predate Server Components by years.
What production code looks like: Server-side data stays on the server. In Next.js, that means Server Components for reads and Server Actions for writes. For client-side interactivity, a proper cache layer — React Query, SWR, or the framework's built-in cache — handles deduplication, background refetching, and optimistic updates. The key principle: data fetched from an API is not "state." It's a cache of remote data, and it needs cache semantics.
Pattern 4: No Caching Strategy
What AI generates: Every request hits the database. Every page render triggers a fresh query. There's no HTTP caching headers, no in-memory cache, no CDN configuration, no stale-while-revalidate, no ISR. The app treats every request as if it's the first time anyone has ever asked for this data.
I measured one AI-built SaaS app that hit its Postgres database 2,847 times to render its marketing homepage. The homepage. Static content that changes once a month. Every visitor triggered 2,847 queries because the page was composed of 23 components, each independently fetching its own data, with nested queries for related content.
Why it breaks: Databases have connection limits. A typical managed Postgres instance allows 100-200 concurrent connections. At 2,847 queries per page load, you hit connection exhaustion at roughly 15 concurrent users. The app doesn't crash gracefully — it starts throwing connection timeout errors intermittently, creating the most frustrating class of bug: "works sometimes, fails randomly."
Your hosting bill also explodes. This is part of the hidden cost of vibe coding — infrastructure costs that grow 3-5x higher than they should because of missing caching. Serverless functions billing is based on execution time and invocations. Without caching, every page view costs the same as the first. We've seen startups paying $800/month in Vercel compute costs for apps that should cost $20 with proper caching.
What production code looks like: A caching strategy has layers. Static content gets pre-rendered at build time or cached at the CDN edge. Dynamic content that changes infrequently uses ISR (Incremental Static Regeneration) or stale-while-revalidate headers. Frequently accessed dynamic data — like user session info — uses an in-memory cache like Redis with TTLs measured in seconds. Database queries use connection pooling and prepared statements. The result: the same app handles 10,000 users with fewer database queries than the uncached version uses for 10.
Pattern 5: Synchronous Everything
What AI generates: Every operation happens in the request-response cycle. User submits a form? The route handler sends the email, updates the CRM, generates a PDF, triggers analytics events, and notifies the team — all before returning a response. The user stares at a spinner for 4-12 seconds.
I reviewed an onboarding flow where the "Create Account" button triggered: user creation, welcome email, Slack notification to the sales team, CRM contact creation, analytics event, and a webhook to a third-party integration. Six external network calls, all synchronous, all blocking. If Slack's API was slow — which it regularly is — the user waited 8 seconds to create an account. If Slack was down, the account creation failed entirely, even though Slack is irrelevant to having an account.
Why it breaks: Coupling non-critical operations to user-facing requests means your app is only as reliable as your least reliable dependency. When SendGrid takes 3 seconds to accept an email, every form submission takes 3 extra seconds. When your CRM's API goes down for maintenance, your app's core functionality breaks. This is why AI prototypes break in production — they chain dependencies that should be independent.
Synchronous processing also creates scaling walls. Each long-running request occupies a serverless function instance (or a thread in traditional servers) for its entire duration. Ten users submitting forms simultaneously means ten instances occupied for 4-12 seconds each, instead of ten instances occupied for 200ms each.
What production code looks like: The request-response cycle does the minimum: validate, write to database, return success. Everything else happens asynchronously. Background job queues (BullMQ, Inngest, Trigger.dev) handle email sending, CRM syncing, notifications, and analytics. The user gets a 200ms response. The welcome email arrives 2 seconds later. If Slack is down, the notification retries automatically. The user never knows or cares.
The Common Thread
These five patterns share a root cause: AI tools generate code that satisfies the immediate prompt without architectural context. "Build me a user dashboard" produces a dashboard. It doesn't produce the service layer, caching strategy, background job system, and component architecture that a dashboard needs to survive contact with real users.
This isn't a criticism of the tools. They're extraordinary at velocity. But velocity without architecture is just technical debt generation at superhuman speed.
We've seen this pattern across 50 audits — every app we've reviewed had at least four of these five patterns. The apps that survived production were the ones where a human engineer refactored the architecture before scaling.
The fix isn't to stop using AI tools. It's to pair them with someone who knows what production architecture looks like — and can restructure what the AI generated before your users do the stress testing for you. An architecture review maps every instance of these patterns and produces a prioritized remediation plan.
Frequently Asked Questions
Can AI coding tools learn to produce better architecture over time?
The tools are improving, but the fundamental constraint remains: architecture decisions require understanding business context, scale expectations, and operational requirements that aren't in the prompt. A tool can learn to extract components instead of generating god components, but it can't know whether your data needs real-time synchronization or eventual consistency without understanding your business. Better prompts help. They don't solve the structural problem.
How do I know if my app has these patterns without a full audit?
Open your largest component file and count the lines. If any component exceeds 200 lines, you likely have god components. Open three API route handlers — if any contain database queries directly, you're missing a service layer. Check your Network tab on any page — if you see duplicate requests or the same endpoint called multiple times, you have client-side data fetching problems. Run your app with 50 simulated concurrent users — if response times exceed 2 seconds, you likely have caching and synchronous processing issues.
Is it worth refactoring, or should I rebuild from scratch?
Refactor. Almost always. We have the data to back this up in our rebuild vs rescue engineering comparison. The business logic in your AI-generated code is correct — it's the architecture wrapping it that's broken. Extracting a service layer from route handlers takes days, not weeks. Adding a caching layer is a few hours of work. Setting up a background job queue is a one-day project. Rebuilding from scratch means re-implementing every feature, re-testing every flow, and re-discovering every edge case your current code already handles.
Which pattern should I fix first?
Start with the missing service layer (Pattern 2), because it's the prerequisite for everything else. You can't add proper caching or background jobs until your business logic is extracted into reusable services. Then add caching (Pattern 4) for the biggest performance wins. Background job queues (Pattern 5) come next. Component refactoring (Patterns 1 and 3) can happen incrementally alongside feature development.
How do these patterns affect my ability to hire engineers?
Significantly. Senior engineers evaluate codebases during the interview process. A god-component architecture with no service layer signals "I'll be rewriting this from day one." We've seen candidates decline offers after reviewing AI-generated codebases — not because the code doesn't work, but because working in that codebase daily would be painful. Clean architecture isn't vanity. It's a hiring asset.
Can I use better prompts to avoid these patterns?
Better prompts produce incrementally better results. Prompting "create a user dashboard using a container/presenter pattern with a service layer and React Query for data fetching" will produce better architecture than "create a user dashboard." But you need to know what good architecture looks like before you can prompt for it — which puts you back at needing architectural expertise. The prompt is only as good as the prompter's engineering judgment.
Architecture decisions compound. Every feature built on top of a god component makes the god component harder to refactor. Every route handler with inline business logic makes the missing service layer harder to extract. These patterns don't just slow you down today — they accelerate the rate at which your codebase becomes unmaintainable.
If you're shipping an AI-built app to paying users, the architecture needs a human review before scale reveals what demos hide. Talk to us about a production readiness audit — we'll map every instance of these patterns and give you a prioritized remediation plan.