AI Writes Code That Compiles but Breaks Your Architecture: Here's Why

· 9 min read

Part 3 of a 6-part series on configuring AI coding assistants for large codebases


Imagine you ask your AI assistant to add a feature that checks inventory before confirming an order. It reads OrderService.java. It reads InventoryDao.java. It generates code where OrderService calls InventoryDao directly.

The code compiles. The types align. The logic is correct. But it violates your architecture. Orders never query the inventory database directly. They call InventoryService, which handles stock locking and warehouse sync. The DAO shortcut skips all of that.

The AI had access to every file in the project. It still got the architecture wrong. Why?

Because architecture isn’t in the code. It’s in the gaps between the code.

What AI can’t infer

AI assistants are good at reading code. They can trace function signatures, follow import paths, match type hierarchies, and pick up naming patterns. Give an AI a well-written Dropwizard resource class and it’ll generate a similar one that follows the same structure.

But there’s a category of knowledge that doesn’t exist in any file. Which module owns which concern. Which dependency directions are allowed and which are forbidden. Why two similar-looking classes exist separately instead of sharing a base class. Which patterns are current and which are deprecated but haven’t been deleted yet.

This is the stuff that lives in architects’ heads. In Slack threads from two years ago. In onboarding conversations that were never written down. The AI can’t infer any of it.

A useful rule of thumb: if two senior engineers on your team might disagree about it, the AI definitely needs it documented.

Module boundaries

If you document only one architectural concept in your instruction file, make it module boundaries. This prevents more AI mistakes than anything else.

A module boundary answers two questions. What does this module own? And what is it not allowed to do?

## Module boundaries
### orders/
Owns: order lifecycle: creation, payment capture, fulfillment, cancellation.
Allowed deps: calls InventoryService and UserService. Publishes to `order.events` Kafka topic.
Never: queries inventory or user tables directly. Never sends notifications.
### inventory/
Owns: stock levels, warehouse sync, stock reservations.
Allowed deps: none. This is a leaf module. Other modules call it.
Never: modifies order state. Never accesses billing tables.
### billing/
Owns: invoices, audit trails, revenue reporting.
Allowed deps: reads from order and inventory data via read replicas.
Never: mutates data in other modules. Audit records are append-only.
### notifications/
Owns: email, SMS, push delivery. Stateless async workers.
Allowed deps: consumes from Kafka topics. Calls external provider APIs.
Never: has direct database access. Never imports from other modules.

This is about 20 lines. An AI reading this knows that OrderService should call InventoryService, not InventoryDao. It knows notifications doesn’t have a database. It knows billing uses read replicas and never mutates other modules’ data.

Without these boundaries, the AI does what any reasonable developer would do. It takes the shortest path. If InventoryDao is importable, it’ll import it. The boundary rules tell it which shortcuts are off-limits.

Dependency direction maps

Module boundaries say what each module owns. Dependency directions say how modules are allowed to talk to each other.

In a layered Dropwizard service, this is usually a one-way graph:

## Dependency direction (one-way only, never reverse)
resources/ -> service/ -> dao/ -> database
Cross-module calls:
orders/service -> inventory/service (direct method call via HK2 injection)
orders/service -> billing/service (direct method call)
orders/service -> notifications (async via Kafka topic, never direct call)
inventory/service -> (no outgoing cross-module calls)
billing/service -> (no outgoing cross-module calls)

This is the kind of information that exists in an architect’s head but nowhere in the code. The import graph might show that OrderService currently calls InventoryService. But it doesn’t tell the AI that InventoryService should never call OrderService back. Only the direction map makes that explicit.

Without it, the AI might add a callback from inventory to orders. It would look reasonable. It would compile. And it would create a circular dependency that breaks your architecture.

Signatures over implementations

Sometimes the AI needs to understand the shape of a codebase without reading every file. Full file reads are expensive. Each one costs 500 to 5,000 tokens from your context budget. In a 150K-line project, you can’t afford to have the AI read everything.

Function signatures give structural awareness at a fraction of the cost:

## Key service interfaces
OrderService.java:
- createOrder(CreateOrderRequest req): Order
- capturePayment(String orderId, PaymentDetails payment): PaymentResult
- cancelOrder(String orderId, String reason): void
- getOrderStatus(String orderId): Optional<OrderStatus>
InventoryService.java:
- reserveStock(String sku, int quantity): ReservationResult
- releaseReservation(String reservationId): void
- getStockLevel(String sku): StockLevel
UserService.java:
- authenticate(Credentials credentials): AuthResult
- getUserProfile(String userId): Optional<UserProfile>
- updatePreferences(String userId, Preferences prefs): void

Ten signatures give the AI more useful context than two full file reads. It now knows the method names, parameter types, and return types for every major service. When it generates code that calls InventoryService, it’ll use reserveStock() with the right parameters instead of inventing a method name.

Update these when APIs change. Stale signatures are worse than none. A wrong method name that the AI trusts because the instruction file told it so is harder to catch than a hallucinated one.

Pointers, not content

Architecture documentation can be detailed. Your OpenAPI spec might be 2,000 lines. Your database migration history might span hundreds of files. Your sequence diagrams might fill a wiki.

None of that belongs inline in your instruction file.

The principle: point to documentation, don’t embed it. Let the AI read the full document on demand.

## Architecture references
- System design overview: see docs/architecture.md
- API contracts: see resources/openapi.yaml
- Database schema: see db/migrations/V42__latest_schema.sql
- Order state machine: see docs/order-state-diagram.md
- Event schemas: see docs/kafka-event-schemas.avsc

Think of this like a library card catalog. You don’t photocopy every book and tape it to the catalog. You write a short description and a shelf number. The AI pulls the book only when it needs it.

Five pointer lines replace potentially thousands of lines of inline documentation. The AI reads openapi.yaml only when it’s working on an API endpoint. It reads the schema migration only when it’s writing a DAO. The rest of the time, those files consume zero context budget.

Documenting design decisions

Code shows what was built. It rarely shows why. And the “why” is often what prevents the AI from making a plausible-but-wrong decision.

Architecture Decision Records (ADRs) are perfect for this. You don’t need to embed them. Just point to the ones that matter.

## Key design decisions
- Why we use JDBI3 instead of Hibernate: see docs/adr/003-jdbi-over-hibernate.md
(Summary: explicit SQL control, no lazy loading surprises, simpler transaction management)
- Why orders and billing are separate modules with no shared tables:
see docs/adr/007-billing-separation.md
(Summary: independent scaling, audit isolation, different retention policies)
- Why notifications are async via Kafka instead of synchronous:
see docs/adr/012-async-notifications.md
(Summary: order confirmation must not block on email delivery)

That one-line summary per ADR is key. The AI reads it and often has enough context to proceed without opening the full document. If it does need more detail, it follows the pointer.

Focus on decisions that are counterintuitive. “We use JDBI3 instead of Hibernate” matters because any Java AI assistant will default to JPA/Hibernate. That’s what the majority of its training data uses. If you don’t explain the choice, the AI will fight you on it by suggesting Hibernate patterns over and over.

Common architectural mistakes

The same mistakes come up across almost every large Dropwizard project. All of them trace back to missing architectural context.

Shortcutting the service layer. The AI sees that OrderResource needs data from the database. It imports OrderDao directly, bypassing OrderService. This skips transaction management, validation, event publishing, and whatever else lives in the service layer. A one-line boundary rule prevents it: “Resources never call DAOs directly. Always go through the service layer.”

Creating circular dependencies. The AI adds a call from InventoryService to OrderService because the task requires knowing order details. Makes sense locally. Breaks the dependency graph globally. The dependency direction map catches this.

Merging things that look similar. Two resource classes have similar structure. The AI extracts a common base class. But they’re separate for a reason: maybe different auth requirements, different rate limits, different compliance flows. The instruction file should call this out explicitly when it applies.

Using the wrong persistence pattern. The AI knows Hibernate. It’s seen a lot of Hibernate code. When you ask it to write a DAO, it reaches for EntityManager and @Entity. If your project uses JDBI3, the design decision pointer saves you from correcting this every single time.

Keeping context fresh

Architecture docs rot faster than code. Nothing fails when they’re wrong. No test turns red. No build breaks. The docs just silently drift from reality.

Two practices help.

First, assign ownership. The person who owns inventory/ should own inventory/CLAUDE.md. When they change the module, they update the instruction file. Make it part of the definition of done for architectural changes.

Second, do a quarterly audit. Read through the instruction file and ask: does the package map still match reality? Do the dependency directions hold? Are the function signatures current? Did we add a new module that has no instruction file?

This takes an hour per quarter. It’s cheap insurance against an AI that’s operating on a stale map of your codebase.

The 80/20 rule

You don’t need to document everything. Most AI architectural confusion comes from three sources:

  1. Unclear module boundaries. The AI doesn’t know which module owns the concern.
  2. Wrong dependency directions. The AI takes the shortest path instead of the allowed path.
  3. Stale or missing API contracts. The AI invents method signatures or endpoint paths.

Document these three clearly and concisely. You’ll eliminate the majority of cross-module hallucination. The full ADR library, the sequence diagrams, the wiki pages. Those are nice to have. The boundary rules and dependency directions are need to have.

Eighty percent of the value comes from twenty percent of the documentation. Write that twenty percent well and keep it current.


Next up: Post 4: Why AI Hallucinates Imports (and How to Stop It), covering the mechanics behind phantom packages, version drift, and cross-module confusion, with concrete prevention strategies.

Subscribe to the Newsletter

Get notified when new posts are published. No spam, unsubscribe anytime.

Coming soon.