Office Hours — How do you maintain code quality and architectural control when using AI to generate most of your codebase? A daily developer question about AI/LLMs, answered with a direct, opinionated take. 2026-07-18T12:00:00.000Z Office Hours Office Hours office-hoursq-and-apractical-ai

Office Hours — How do you maintain code quality and architectural control when using AI to generate most of your codebase?

A daily developer question about AI/LLMs, answered with a direct, opinionated take.

Daily One question from the trenches, one opinionated answer.

How do you maintain code quality and architectural control when using AI to generate most of your codebase?

This one lands differently than the usual “LLM goes brrr” conversations. You can’t just turn on code generation and hope. The moment you offload >50% of output to Claude or Codex, your architecture either tightens or it collapses.

The Real Problem Isn’t the AI, It’s Entropy

AI-generated code has a specific failure mode: it optimizes locally for “make this function work” without seeing the system you’re building. You get a thousand small decisions that are individually reasonable but collectively incoherent. Duplicated abstractions. Inconsistent error handling. Three different ways to validate input across your codebase because each generated function solved its own problem in isolation.

A senior engineer would catch this in code review. But code review doesn’t scale when you’re reviewing 10K lines of generated code per week. You either automate your standards or you lose control.

Layered Enforcement: Static > Runtime > Human

The teams shipping production code with heavy AI generation use a three-layer defense:

First, static enforcement. Linters, formatters, type checkers—these are your fast feedback loop. Configure ESLint, Mypy, or Go’s gofmt to be strict enough that generated code either conforms or the CI pipeline rejects it before a human even sees it. This isn’t new, but it’s non-negotiable. When I talked to an infrastructure team at a fintech that’s having Claude write Terraform, they set up a pre-commit hook that runs terraform validate, tfsec, and custom policy checks. Generated code that violates those constraints never makes it to the branch.

Second, architectural constraints baked into your codebase structure. This is the unsexy part that actually matters. Use dependency injection, module boundaries, or package-level isolation to constrain where generated code can reach. If you’re using Claude Code or Cursor Agent to modify a monolith, create a clear interface layer—generated code lives inside domain modules; it doesn’t touch your request router, authentication middleware, or database connection pool. Document this boundary in a ARCHITECTURE.md that sits in your repo root and gets fed into the model’s context window.

Third, the human loop. But make it targeted. Instead of reviewing every function, review only the generated code that crosses architectural boundaries, modifies shared state, or handles security-sensitive logic. Code that generates a CRUD endpoint inside a well-isolated module and passes all tests? Ship it. Code that rewires your middleware chain or adds a new database query to an ORM that’s used across the service? That gets review.

Concrete Pattern: Specification-Driven Generation

The strongest signal from teams that maintain control is this: they don’t ask the AI “build me a user service.” They ask it to “implement the UserRepository interface defined in user.go by writing the FindByID, Create, Update, and Delete methods. Use database/sql. Return *User or *UserNotFoundError. Wrap all SQL errors with fmt.Errorf.”

When you spec the interface first, you’ve already decided the architectural shape. The AI fills in the implementation details. You get determinism without losing the speed of generation.

Here’s what that looks like in practice:

// user.go — your spec, human-written
package user

type Repository interface {
	FindByID(ctx context.Context, id string) (*User, error)
	Create(ctx context.Context, u *User) error
	Update(ctx context.Context, u *User) error
	Delete(ctx context.Context, id string) error
}

type User struct {
	ID        string
	Email     string
	CreatedAt time.Time
	UpdatedAt time.Time
}

Then you tell Claude or Codex: “Implement the Repository interface for PostgreSQL. Use database/sql. All SQL errors should be wrapped with fmt.Errorf. No raw error returns.” You get back a concrete implementation that’s locked into your shape. No surprise abstractions. No deviation.

Pair this with a linter rule (even a simple custom regex in your CI) that enforces the interface is always the source of truth: if the interface changes, the implementation must be regenerated or updated in review.

The Context Rot Problem

One thing that breaks harder with heavy AI generation is long-running agents. The Daily Signal coverage from July 15 called this out directly: Claude Code sessions degrade in quality well before hitting token limits. Context rot. The agent loses sight of your architectural intent as the conversation drifts.

The fix is architectural anchoring in your system prompt. Don’t just tell the agent “follow the style guide.” Embed a PRINCIPLES file:

# Architectural Principles (fed into every Claude Code session)

1. All data access goes through Repository interfaces defined in the domain package.
2. Error handling: wrap with `fmt.Errorf("context: %w", err)`. Never pass raw errors.
3. No circular dependencies. Check with `go mod graph` before committing.
4. Requests are validated at the handler boundary. Domain logic assumes valid input.
5. Tests use table-driven patterns. Fixtures go in `testdata/`.

You prepend this to every agent prompt. It costs a few hundred tokens of context. It’s worth it because it constrains drift.

Cost and Signal

A team generating 40% of their code with Claude Opus 4.8 told me they spend $0.80/task average (factoring in reruns and fixes) but save 18 hours of development per week. The constraint here isn’t money, it’s architectural coherence. They catch misalignment in code review or CI, not in production. That’s the non-negotiable line.

What Actually Fails

AI-generated code breaks hardest when:

  • You have implicit conventions. “We always validate at the handler layer” doesn’t survive AI generation unless it’s documented and checked in CI.
  • Your codebase is highly interconnected with lots of implicit state. Monoliths work; services with clear boundaries work. Tangled balls of code don’t.
  • You’re asking the AI to make architectural decisions instead of fill in a shape you’ve already specified.

The teams that lose control are the ones asking Claude to “refactor this service” or “improve this module.” The teams that maintain it ask Claude to “implement this interface” or “add this test case matching the pattern in testdata/example.go.”

Bottom line:

Treat AI generation like you’d treat junior developers: set clear boundaries through interfaces and linters, anchor your architectural principles in a context file that stays with every request, and reserve human review for code that crosses those boundaries or changes the spec. The AI isn’t the liability. Entropy is. Control entropy through structure, not through manual oversight.

Question via Hacker News