Your Architecture Is Why Your Coding Agent Keeps Writing Bad Code
A few months ago I realized that as the code and modules inside the structure weโd built kept growing, it was starting to become a problem, and developing with AI was getting really hard.
Iโd ask Cursor or Claude Code to make a very simple change to a frontend module. At first glance it looked great. Then Iโd open the PR and inspect it: it had done state management differently from the rest of the app, it had written copies of helper functions that already existed, it had pulled some random third-party package we never use into the project, and it had completely ignored the configs weโd added for design.
At first I thought it was the LLMโs fault, because everyone around me was complaining about this too โ model A is terrible, model B is too expensive, that kind of thing. I looked for the problem in the small context or in the model hallucinating, but the problem was entirely in our architecture.
If I had written the code myself, I would have known easily what goes where, since I set up the architecture, and I could apply everything in line with it. But if I donโt explain these things to the AI, how is it supposed to know, right? If it inspected all the code, the context would bloat, and this time we might get worse results than the ones we got without following the architecture at all. If our codebase is complex, the agentโs context bloats and its reasoning breaks down fast.
If we want AI agents to write clean, production-ready code, just writing better prompts or waiting for the next model update isnโt enough. We need to build agent-native frontend architectures.
Here are the ways to make our architecture ideal for AI agents.
1. Monorepo as a Context Management Strategy
Monorepos used to be used for trunk-based development โ to shorten development processes when multiple teams were developing a single platform, and so on. But now thousands of agents are developing for a single platform. Where it used to make three teamsโ work easier, now thousands of agentsโ work gets easier.
If youโve split your micro frontends into separate repos, then to use a util from repo A โ or something similar to it โ in repo B, you have to download both repos and include them in the context. In that case, even for a very simple task, you lose time, you spend tokens, and sometimes you canโt even reach a result.
In a monorepo (we use Turborepo with pnpm workspaces), everything lives under one roof, but with explicit boundaries:
repo/
โโโ apps/
โ โโโ shell/ # Host application (thin orchestration only)
โ โโโ module-orders/ # Independent micro frontend
โ โโโ module-catalog/ # Independent micro frontend
โโโ packages/
โ โโโ ui/ # Shared component library (shadcn/tailored)
โ โโโ utils/ # Pure helper functions (formatters, calculations)
โ โโโ analytics/ # Single source of truth for tracking schema
โ โโโ config/ # Base tsconfig, eslint, tailwind presets
โโโ turbo.json
โโโ package.json
When I ask an agent to work on module-orders, it doesnโt scan the whole repository. It reaches only the shared packages/ folder it actually uses, directly through local workspace references (workspace:*).
On a single repository, the agent can move around freely and reach what itโs looking for easily.
2. Independent Micro Frontends: Enforcing Strict Boundaries
This is actually the hardest part of a monorepo โ that the boundaries arenโt drawn with strict rules. If we donโt separate the modules and packages from each other properly, development can happen in places we donโt want during the process, and that development can make it all the way to prod, and as you know, the result is an incident.
The solution to this problem is completely independent micro frontends and packages.
The core rule is strict: modules donโt connect horizontally to each other. An agent working on module-orders should never need to know anything about module-catalog or import from it. They connect only vertically, through shared packages and the shell application.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ shell โ โ Orchestration & routing only
โโโโโโโโโฌโโโโโโโโโโโโโฌโโโโโโโโโ
โ โ
โโโโโโโโโโโผโโโโโ โโโโโผโโโโโโโโโโ
โ orders โ โ catalog โ โ Completely isolated modules
โโโโโโโฌโโโโโโโโโ โโโโโโโโฌโโโโโโโ
โ โ
โโโโโโโโโโฌโโโโโโโโโโโ
โผ
โโโโโโโโโโโโโโ
โ packages/ โ โ Shared, versioned foundation
โโโโโโโโโโโโโโ
The Version Decoupling Trick
To prevent a change in a shared helper from breaking all modules at once, we decoupled our internal packages from each other.
When packages/utils ships v2.1.0 with a new formatCurrency helper:
module-orders moves to โutilsโ: โ2.1.0โ right away.
module-catalog stays on โutilsโ: โ2.0.0โ until we explicitly decide to update it.
As for why this matters for AI: when I tell Cursor or Claude โwork only inside apps/module-orders,โ the boundaries are physically real. The agent canโt break the catalog module, because thereโs no horizontal import path connecting them.
This way, we ended up using isolation as a security mechanism at the same time.
3. Tiered AGENTS.md: Preventing Convention Drift
If you donโt give an AI agent explicit rules, it falls back on the average of its training data, which usually means generic, Stack Overflow-style code from 2024. It writes raw fetch calls instead of your custom API wrapper, or inline Tailwind classes instead of your design configs โ maybe it even writes CSS.
As a solution, AGENTS.md (or .cursorrules) is used. But thereโs a point to watch out for: not every rule should be always-allow. Only the ones you want to run every single time should be.
If you make all the rules always-allow, youโve bloated the context before you even give your own prompt, and as a result youโve dropped your efficiency quite a bit. Research shows that after roughly 3,000 tokens of context load, reasoning quality drops noticeably.
We solved this with tiered rule management:
.rules/
โโโ always/
โ โโโ global-conventions.md # Universal rules (naming, tech stack, base TS config)
โโโ modules/
โ โโโ orders.md # Specific ONLY to the orders domain
โ โโโ catalog.md # Specific ONLY to catalog behavior
โโโ packages/
โโโ analytics.md # Event tracking contracts
โโโ utils.md # Helper function usage rules
An example of a strict package rule (analytics.md):
markdown
# packages/analytics usage rule
- NEVER call `window.dataLayer.push` directly.
- ALWAYS use the exported `trackEvent()` utility from `packages/analytics`.
- Event names MUST be defined in `packages/analytics/events.ts`.
- Required fields for every event: `event_name` (snake_case), `category`, `module`.
When an agent edits an analytics file, it loads analytics.md. When it edits a CSS theme, it ignores the analytics rules completely.
General rule: write the rule the moment you establish a new pattern, not six months later. If you forget, you wonโt remember it again โ proven by experience :) Pair this with strict linters so the agent gets immediate feedback when it violates a rule.
How I Prompt Agents Now
When your architect