Search the site
Find pages, case studies and writing
Skip to content
All writing

Valid code in the wrong layer is still bad code

How I turned architecture boundaries into executable checks, so coding agents could make substantial changes without every plausible shortcut becoming the next bad pattern in the codebase.

On a reinsurance underwriting platform I built, a boundary checker enforces 36 blocking rules on every deploy. After handover it has blocked 8 deploys that broke them, and the client’s team extended the rules rather than switching them off. The results are here. This post is about how it works.

Valid code in the wrong layer is still bad code. Coding agents can now produce a lot more of it before lunch.

An architecture document is not a force field

The product was split across multiple applications and packages. Each part had a clear job, but writing that down in an architecture document does not stop an agent putting a convenient shortcut in the wrong place.

The shortcuts are predictable. Say you have some data in a database. A lot of the time, an LLM will fetch that data inside the component, with a client-side fetch, when it should be using a load function or a remote function. It’ll solve a date formatting problem locally in whatever file it’s in. And in Svelte it’ll reach for $effect to manage state, because it looks like React. $effect is an escape hatch for things like third-party libraries and analytics. It’s not for managing state.

None of those break the build on their own. They just pile up.

Turning boundaries into checks

So I turned those boundaries into executable policy. The checker parses the TypeScript and Svelte source, rather than grepping it, and fails when, for example:

  • one runtime application imports another
  • a route mutates product data directly
  • a protected database table is written outside its declared owner
  • a sensitive operation bypasses the expected authorisation boundary

The 36 blocking rules fall into a few groups: import boundaries between apps and packages, no domain logic or database access in routes, the shape of remote function modules, and SQL only in the database package.

Alongside it:

  • Custom lint rules, including one that makes every $effect carry a comment justifying it.
  • A route data audit that catches data being loaded twice and flags stale exceptions.
  • Ownership checks: protected tables have a single declared writer, and every exception has to cite an issue.

Make the failure useful

When the checker fails, it points to the file, explains which boundary was crossed and says where that responsibility belongs.

That matters more than it sounds. Agents like to run a check at the end of their changes. When the check rejects the change with a clear reason, the agent fixes it itself. That’s much more useful than another instruction telling an agent to use good architecture.

The aim is not to make agents slower. It is to let them make substantial changes without every plausible shortcut becoming the next bad pattern in the codebase.

Where to start

There’s a little bit of work upfront. Make sure the app has clear definitions of where data should come from and what not to write where. Then block the writing of that code with a custom lint rule or a boundary check, so it fails before it ever reaches review.

Pick the three or four mistakes you keep correcting in review, and make the build correct them instead.

It looks like extra work at first. It makes the product ship faster.

I’m paid to ship the product, not to maximise how much code an agent can produce. Those are not the same thing.

If your team is adopting coding agents and you want the architecture to hold, let’s talk.