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.
Because architecture isn’t in the code. It’s in the gaps between the code.
The AI had access to every file in the project. It still got the architecture wrong because the rule that mattered most lived outside the codebase’s visible structure. Nobody had written down that orders must go through InventoryService, not around it.
That’s the pattern behind a lot of AI-generated code that looks right in review and still creates production problems. The assistant follows what the code makes easy to see. It misses what the team treats as obvious but has never made explicit.
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. In practice, this is the fastest way to stop the most expensive class of mistakes.
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 model 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.
One team I worked with learned this the hard way. Their assistant added a direct DAO call inside a resource because it was the shortest path to the data. The patch passed tests, but it bypassed transaction hooks and skipped an event publish that downstream systems relied on. After they added a one-line rule, resources never call DAOs directly, the same class of suggestion stopped showing up.
Dependency direction maps
Module boundaries tell the AI what each part of the system owns. The next layer is dependency direction: how those parts 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 assistant 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.
Once those boundaries and directions are clear, the next problem is cost. You still need to give the model enough structural context to work accurately without making it read half the repository.
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): voidTen signatures give the AI more useful context than two random full file reads because they compress the part that matters most: callable surface area. Instead of reverse-engineering method names from implementation details, the model can see the API shape directly.
That matters in practice. Without a signature list, an assistant might invent holdInventory() because it sounds plausible. With the interface in front of it, it reaches for reserveStock() and usually gets the call right on the first pass.
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
After boundaries, directions, and signatures, the next layer is deeper reference material. This is where many instruction files get bloated.
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.
Point to the documentation instead of embedding 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.avscThink 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 model 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. Often it’s enough context for the AI 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 many Java AI assistants default to JPA/Hibernate. That’s what a lot of their training data uses. If you don’t explain the choice, the model will keep suggesting Hibernate patterns.
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
None of this helps if the map is stale. 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:
- Unclear module boundaries. The AI doesn’t know which module owns the concern.
- Wrong dependency directions. The AI takes the shortest path instead of the allowed path.
- 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, or you’ll keep reviewing code that looks clean right up until it sidesteps the rule your architecture depends on.
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.