AI Context Engineering for Multi-Module Monoliths
Multi-module monoliths are often considered simpler for AI agents than microservices because everything lives in one repository — no cross-repo access issues, no need for external pointer files. But this assumption is wrong once the monolith contains a dozen modules with different domains: an agent working on the payment module can still get flooded with context from the catalog, shipping, or notification modules if there’s no clear structuring. This article discusses context engineering inside a single repository concretely, using a multi-module e-commerce monolith with several main modules as a case study, complete with example AGENTS.md and CLAUDE.md contents at each layer and best practices for keeping context relevant and in sync.
Case Study: Multi-Module E-Commerce Monolith Structure
To make the discussion concrete, let’s use one representative structure for a modular-style e-commerce monolith application — a pattern commonly used in Go (modular monolith with per-domain packages), Java/Spring (multi-module Maven/Gradle), and Node/TypeScript (workspace-based internal monorepos).
ecommerce-monolith/
├── AGENTS.md
├── CLAUDE.md
├── docs/
│ └── architecture.md
├── modules/
│ ├── catalog/
│ │ ├── AGENTS.md
│ │ ├── CLAUDE.md
│ │ ├── domain/
│ │ ├── application/
│ │ └── infrastructure/
│ ├── cart/
│ │ ├── AGENTS.md
│ │ ├── CLAUDE.md
│ │ └── ...
│ ├── checkout/
│ │ ├── AGENTS.md
│ │ ├── CLAUDE.md
│ │ └── ...
│ ├── payment/
│ │ ├── AGENTS.md
│ │ ├── CLAUDE.md
│ │ ├── gateway/
│ │ │ └── AGENTS.md
│ │ └── ...
│ ├── inventory/
│ │ └── ...
│ ├── shipping/
│ │ └── ...
│ ├── user/
│ │ └── ...
│ └── notification/
│ └── ...
└── shared/
├── domain/
└── AGENTS.md
Seven main modules: catalog (products), cart, checkout, payment, inventory, shipping, user, and notification. There’s also a shared/ folder for domain models used across modules — this is deliberately a special case example because it’s often a source of context ambiguity.
Why Multi-Module Monoliths Still Need Context Engineering
Three reasons this structure still needs the same serious context attention as multi-service, even though technically all code is in one accessible place:
The context window is still limited. An agent working on a bug in payment/gateway/ doesn’t need to know the implementation details of notification/email-templates/. Full technical access doesn’t eliminate the need for filtering — tokens are still expensive and noise still degrades answer quality.
Domain knowledge is still separated. The payment module’s conventions (must be idempotent, must have an audit trail, must connect to an external payment gateway) are very different from the catalog module’s conventions (focus on search performance, product caching). Putting all of this in one big file forces the agent to filter out what’s relevant itself — work that should already be done by context file structuring.
Module boundaries are often violated without realizing it. One of the most common problems in modular monoliths is an agent (or developer) unknowingly importing directly from another module’s internals, violating a boundary that should be guarded through public interfaces. Context files that explicitly explain these boundaries reduce that risk.
flowchart TD
A[Agent working in modules/payment/] --> B{What context gets read?}
B -- Without structuring --> C[One giant AGENTS.md containing all modules]
C --> D[High noise, mixed conventions, wasted tokens]
B -- With structuring --> E[Root AGENTS.md: global conventions]
E --> F[modules/payment/AGENTS.md: payment domain conventions]
F --> G[Relevant, lean context with clear boundaries]How Context Actually Works in Various AI Agents
Before getting into the three-layer pattern, it’s important to emphasize: the root/module/local structure discussed in this article is a recommended pattern based on general context engineering principles — not a universal mechanism that works exactly the same in every AI coding agent. Each tool has a different way of determining which instruction files are read, when, and in what order. Understanding these differences matters so the pattern you apply really fits the tool your team uses.
Claude Code — Folder-Location-Based Lazy Loading
The official Claude Code documentation explains that the CLAUDE.md file at the root (along with enterprise and user levels above it) is automatically loaded into context when a session starts. For CLAUDE.md files in subdirectories, the mechanism is different — the file is only loaded when Claude reads a file from that directory, not all at once at session start. If the agent has never touched a file in a folder, the CLAUDE.md in that folder hasn’t been loaded into context either, and its instructions can seem “ignored” when in fact they simply haven’t been read yet. This is what makes the /memory command useful for checking which instruction files have actually entered the context at a given point in a session.
Reference: docs.anthropic.com/en/docs/claude-code/memory and code.claude.com/docs/en/memory.
OpenAI Codex CLI — Cascading AGENTS.md from Global to Working Directory
Codex CLI, one of the early drivers of the AGENTS.md format as an open standard, supports nested AGENTS.md natively. The important difference from Claude Code: Codex composes its context from several layers at once — the global level (~/.codex/AGENTS.md), the repo root level, and the current working directory level — then merges them, with more local files winning conflicts against more general ones. OpenAI’s own Codex repository is reported to use dozens of separate AGENTS.md files scattered across the project’s directories as a real-world example of this pattern applied at scale.
Reference: the official specification at agents.md, and OpenAI developer documentation on custom instructions with AGENTS.md.
Cursor — Not Folder-Location-Based, but Glob Patterns and Semantic Activation
Cursor has the most different approach of the three tools above. Instead of the “file closest to the working location wins” mechanism of Claude Code and Codex, Cursor uses a .cursor/rules/ directory containing .mdc files with four activation modes defined via YAML frontmatter:
| Activation Mode | Trigger |
|---|---|
| Always Apply | Always loaded on every request, regardless of file location |
| Auto Attached | Loaded if the file being touched matches the pattern in the globs field |
| Agent Requested | The AI itself decides whether this rule is relevant, based on the description field |
| Manual | Only loaded if the developer explicitly types @rule-name in chat |
The globs field in Cursor isn’t tied to the folder location where the rule is stored — a rule in .cursor/rules/backend.mdc can be activated by the pattern services/api/**, even though the .mdc file physically lives in a centralized location, not scattered following the code folder structure. This is the opposite of the nested AGENTS.md/CLAUDE.md pattern, which relies on the file’s physical location as the determinant of when it’s loaded.
Reference: official Cursor documentation on Rules and the .mdc frontmatter format.
Implications for the Three-Layer Pattern in This Article
Because of these differences, applying the root/module/local pattern discussed here needs to be adapted depending on the tool:
- Claude Code — the nested
AGENTS.md+CLAUDE.mdbridge pattern (as discussed in the previous section) is directly relevant, because folder-location-based lazy loading is exactly how it natively works. - Codex CLI — nested per-module
AGENTS.mdis also natively supported, without needing an additional bridge file like in Claude Code. - Cursor — the nested per-folder pattern fits less well applied as-is. What fits better is translating module boundaries into
globspatterns in centralized.mdcfiles, or usingAGENTS.mdwhich Cursor also reads as the cross-tool source of truth, while adding dedicated.mdcfiles for the semantic activation needs that are Cursor’s strength.
- These mechanism details change fairly quickly following each tool’s updates — if your team is serious about applying this pattern, re-check the official documentation of the tool in use before final implementation, don’t rely only on this article as the sole source.
- If the team uses more than one tool,
AGENTS.mdas the source of truth remains sensible because it’s widely supported — but the mechanism of how it’s loaded into context still differs per tool, and that determines how effective your nested structuring is in each tool.
Context Layers: Root, Module, and Local
This three-layer pattern is the main framework for all the concrete examples in this article.
| Layer | Location | Contents | Change Frequency |
|---|---|---|---|
| Root | /AGENTS.md | Global conventions, tech stack, how to run/test/build, overall architecture | Rarely |
| Module | /modules/<name>/AGENTS.md | Domain-specific conventions, module boundaries, business invariants | Medium |
| Local | /modules/<name>/<sub-folder>/AGENTS.md | Very specific details — e.g. a particular payment gateway integration | Often |
The principle: the deeper the level, the more specific and the more frequently changing its contents. The root file should be stable — if the root changes often, that’s a sign there’s content that should have been moved to the module level.
Concrete Scenario: An Agent Fixing an Idempotency Bug in Payment
To see the benefit of this structuring in action, imagine the following task: there’s a bug where a refund sometimes gets deducted twice if a duplicate webhook arrives from the payment gateway. The agent is assigned to fix it in modules/payment/gateway/.
Without structuring (one giant AGENTS.md at the root containing all modules), the agent has to process all the catalog, cart, shipping, user, and notification conventions before reaching the relevant information — even though this task doesn’t touch those modules at all. Besides wasted tokens, there’s a subtler risk: the agent might “imitate” a pattern from another module that doesn’t actually fit the payment context, for example assuming all modules may update status directly when payment has a strict state machine rule.
With three-layer structuring, the loaded context order naturally follows the task’s needs:
- The root
AGENTS.mdloads at session start — the agent knows general cross-module rules and the location of thepaymentmodule. - As soon as the agent starts reading files in
modules/payment/,modules/payment/AGENTS.mdalso loads — the agent knows the idempotency invariant and state machine rules before touching code. - As soon as the agent enters
modules/payment/gateway/, the local file there also loads — the agent immediately knows that Midtrans webhooks can indeed be duplicates, and validation must go throughtransaction_id + status, not by assuming idempotency on the provider’s side.
This third point matters: without that local file, the agent would most likely spend time deducing from code why webhooks can be duplicated — or worse, make a wrong assumption and add an incorrect state check. Specific gotchas like this are the most valuable type of information to write explicitly in local context files, because they’re hard to deduce just from reading code.
| Approach | Context loaded | Relevance to the task |
|---|---|---|
| One giant AGENTS.md | All 7 modules + shared, all at once up front | Low — most of it irrelevant |
| Three layers (root + module + local) | Root, then payment, then gateway — gradually as needed | High — each layer is relevant to the current task step |
Supporting Tooling for Large Repos: Symbol Index and Search
Structured context files solve the problem of what conventions apply, but don’t solve the problem of where the relevant code is once the repo is very large. For monoliths with tens of thousands of lines per module, an agent that has to read many files one by one to find a function definition or its callers wastes context in a different way — not from context files, but from code exploration itself.
Some tools that help at this point, complementing context files:
- Fast ripgrep/grep — for searching specific strings or patterns across modules without opening each file one by one. Many modern coding agents already use this automatically through built-in tools.
- ctags/tree-sitter symbol index — lets the agent “jump” directly to function/struct definitions, instead of reading an entire file to find one definition.
- Codebase indexing/embedding search — some tools (built into some agents, or external like Sourcegraph) semantically index the whole repo, so a query like “where is the payment gateway retry logic” can point directly to the right file without the agent manually traversing folder structure.
This tooling doesn’t replace context files — it complements them. Context files answer “what are the rules and gotchas here”, while symbol index/search answers “where is the relevant code”. For a monolith the size of this e-commerce case study (seven modules, likely tens of thousands of lines each), the combination of both is far more effective than relying on the agent manually reading entire directories every time it starts a new task.
Concrete: Root AGENTS.md for the E-Commerce Monolith
The root AGENTS.md contains what’s relevant to all modules — not the details of any one domain.
# AGENTS.md — ecommerce-monolith
## Stack
- Go 1.23, modular monolith (not full clean architecture — see docs/architecture.md)
- PostgreSQL per module (separate schemas, no shared tables across modules)
- Every module must expose a public API through the application/ package,
never import directly from another module's domain/ or infrastructure/.
## Module Structure
- catalog/ → product management, categories, search
- cart/ → shopping cart, per-session
- checkout/ → order process orchestration from cart to payment
- payment/ → payment gateway integration, transaction state machine
- inventory/ → stock, reservations, warehouse synchronization
- shipping/ → shipping cost calculation, delivery tracking
- user/ → authentication, profiles, addresses
- notification/ → email, push notifications, SMS
Each module has its own AGENTS.md at modules/<name>/ — READ that file
before making changes in that module. Don't assume one module's
conventions apply the same way in another module.
## Build & Test
- `make test-module MODULE=<name>` to test a single module
- `make test-all` for the whole monolith (slow, avoid unless needed)
- All migrations via `make migrate MODULE=<name>`, don't edit schemas manually
## Cross-Module Rules
- Inter-module communication MUST go through interfaces in application/, never
through direct access to another module's database.
- Domain models used across modules (Product, Order, User) live
in shared/domain/ — see shared/AGENTS.md before changing them.
Note: this root file doesn’t explain payment business details or catalog search algorithms — that’s the business of module-level files.
Concrete: AGENTS.md per Module
Example — modules/payment/AGENTS.md
# AGENTS.md — payment module
## Responsibility
This module handles the entire payment transaction lifecycle: charge,
refund, and reconciliation with the external payment gateway.
## Invariants That MUST NOT Be Violated
- Every charge/refund operation MUST be idempotent — use the idempotency_key
that already exists in the transactions table, don't create a new mechanism.
- Transaction state MAY ONLY change through the state machine in
domain/transaction_state.go — don't update status directly.
- Every status change MUST write an audit log to the audit_trail table
as part of the same database transaction (not a separate best-effort write).
## Dependencies on Other Modules
- Reads order data from the checkout module through the application/OrderReader
interface — does NOT access the orders table owned by checkout directly.
- Publishes payment.completed and payment.failed events consumed
by the checkout and notification modules.
## Payment Gateway
External gateway integration lives in the gateway/ sub-folder — see
gateway/AGENTS.md for provider-specific details.
## Tests
- Use the mock gateway in testing/mock_gateway.go for unit tests.
- Idempotency tests are MANDATORY in every PR touching the charge/refund flow.
Example — modules/catalog/AGENTS.md
# AGENTS.md — catalog module
## Responsibility
Product, category, variant attribute management, and search indexing.
## Module-Specific Conventions
- Every product schema change MUST be accompanied by a reindex to Elasticsearch
(see infrastructure/search_indexer.go) — changes that don't trigger
a reindex will make search data stale.
- Prices and stock are NOT stored in this module — prices live in the external
pricing service (see shared/AGENTS.md), stock lives in the inventory module.
## Dependencies on Other Modules
- Reads stock from the inventory module through the application/StockReader
interface, never query the inventory table directly.
## Performance
- Search endpoints have a p99 SLA of < 200ms — any query path change
must be accompanied by a benchmark before merging.
Note the style difference between payment and catalog — each focuses on the invariants and gotchas truly specific to its domain, not repeating general conventions already in the root.
Example — modules/payment/gateway/AGENTS.md (Local, Most Specific)
# AGENTS.md — payment gateway integration
## Provider
Current integration is with Midtrans (not Stripe/Xendit — migration was
already attempted, see docs/architecture.md#adr-012 for why it was cancelled).
## Important Gotchas
- Midtrans webhooks can arrive duplicated — always validate via
transaction_id + status, don't assume webhooks are idempotent
on their side.
- The Midtrans sandbox has strict rate limits (10 req/minute) — don't
run integration tests without mocks unless you truly need to.
## Credentials
Credentials are in the vault, not config files — see
infrastructure/README.md for how to access them locally.
A local file like this should ideally only be read when the agent actually touches the gateway integration — not every time it works in the payment module in general. This is what makes the nested pattern (rather than one big file) important.
Root CLAUDE.md and Bridges to Every Module
Because Claude Code natively lazy-loads files named CLAUDE.md in subdirectories — not AGENTS.md — you need a bridge file at every level that also has an AGENTS.md, so that automatic discovery mechanism gets triggered too.
# CLAUDE.md — root
@AGENTS.md
# modules/payment/CLAUDE.md
@AGENTS.md
# modules/payment/gateway/CLAUDE.md
@AGENTS.md
Each CLAUDE.md is just one import line. This avoids content duplication — AGENTS.md remains the single source of truth that other tools can also read (Cursor, Codex CLI, Windsurf), while CLAUDE.md only acts as a trigger so Claude Code also discovers that file automatically when working in that folder.
If you don’t want to maintain duplicate files at all, an alternative is symlinks:
cd modules/payment && ln -s AGENTS.md CLAUDE.md
cd gateway && ln -s AGENTS.md CLAUDE.md
sequenceDiagram
participant Agent
participant CC as Claude Code
participant Root as /CLAUDE.md
participant Mod as modules/payment/CLAUDE.md
Agent->>CC: Start session at root
CC->>Root: Load @AGENTS.md root (startup)
Agent->>CC: Task: fix bug in modules/payment/
CC->>Mod: Read files in this subtree
Mod-->>CC: Lazy-load modules/payment/CLAUDE.md -> @AGENTS.md
CC-->>Agent: Combined context: root + payment module
- The root
CLAUDE.md/AGENTS.mdloads at session start. Nested files only load when Claude reads a file in that subtree — not automatically all at once up front.- If after working in a folder the nested instructions seem not to have been read, run
/memoryto check which files have actually been loaded.
Determining Module Boundaries: When a Folder Needs Its Own AGENTS.md
Not every sub-folder needs its own AGENTS.md. Practical rules:
NEEDS its own AGENTS.md if the folder:
✓ Has domain/business rules different from its parent
✓ Is often the site of agent work separately from other folders
✓ Has specific gotchas/invariants that don't apply elsewhere
✓ Is a team ownership boundary (even in one repo, different teams)
DOESN'T NEED it if:
✗ Its contents are just generic files (e.g. a small utils/ folder)
✗ Its conventions are exactly the same as the parent
✗ It's rarely touched, and when it is, 1-2 extra lines
in the parent file suffice
For the e-commerce case study above, payment/gateway/ deserves its own file because the external provider, webhook gotchas, and sandbox rate limits are very specific and dangerous to ignore. Conversely, folders like payment/testing/ containing only mock helpers don’t need a separate file — a one-line mention in payment/AGENTS.md suffices.
The harder question usually appears for folders in the middle — not clearly needing it, not clearly not. For ambiguous cases like this, a useful heuristic is to ask: “if a new developer were assigned to work exclusively in this folder for a week, what information would they ask for repeatedly on the first day?” If the answer is a lot and specific, that’s a signal the folder deserves its own context file. If the answer is “the same as what’s already in the parent”, just fold it into the existing file.
It’s also important to remember this decision isn’t made once. A module that was originally small and explainable in one parent line can grow complex over time — for example inventory/reservation/, which was simple at first, can get complicated once multi-warehouse reservation logic is added. Periodically reviewing context files (ideally alongside module architecture reviews) helps catch the moment when a sub-folder is ready to “graduate” into its own context file.
Directory-Purpose Lines vs Full File Trees
A common mistake when writing context files for large repos is trying to list the entire file tree in detail. This wastes tokens and goes stale quickly as soon as new files appear.
// ANTI-PATTERN — full file list, easily stale
modules/payment/
├── domain/
│ ├── transaction.go
│ ├── transaction_state.go
│ ├── refund.go
│ ├── charge.go
│ └── transaction_test.go
├── application/
│ ├── payment_service.go
│ ├── order_reader.go
│ └── ...
[30 more lines]
// CORRECT — directory-purpose lines, stable against individual file changes
modules/payment/
domain/ → transaction entities & state machine (see invariants above)
application/ → use cases & interfaces to other modules
infrastructure/ → gateway implementation, DB repositories
gateway/ → external provider integration (own AGENTS.md)
testing/ → mocks & fixtures for unit tests
Directory-purpose lines explain the function of each folder in one line, not its full contents. If a new file is added inside domain/, nothing needs updating in the context file — its function stays the same.
Managing Shared Domain Models Across Modules
The shared/domain/ folder is the most common source of ambiguity in modular monoliths — entities like Product or Order are used by many modules, but who “owns” changes to them?
# shared/AGENTS.md
## Entity Ownership
- Product: owned by the catalog module. Other modules ONLY read through
shared/domain/product.go, must not add fields without approval —
update catalog/AGENTS.md too.
- Order: owned by the checkout module.
- User: owned by the user module.
## Change Rules
Every struct change in this folder MUST:
1. Update the AGENTS.md of the owning module
2. Check all consumer modules (see the "// used by" comments on each
struct) for breaking changes
3. Run make test-all (not test-module) because changes here
impact multiple modules
This “explicit ownership” pattern matters so agents don’t casually modify shared entities from any module without realizing the impact on other modules — the same problem as API contracts in microservices, only now the objects are structs in the same repository.
- A shared domain model without explicit ownership is the most common source of breaking changes in modular monoliths — anyone can modify it, nobody feels responsible for validating cross-module impact.
- Don’t put business logic in shared/domain/ — this folder should only contain structs/interfaces; logic stays in the owning module.
Synchronizing Context Files When Modules Change
Just like API contracts in multi-service, context files in monoliths can go stale. The difference: since everything is in one repo, detection mechanisms can be stricter because CI has full access to all changes.
CI Check for Detecting Changes Without Context Updates
flowchart TD
A[PR changes modules/payment/domain/] --> B{Did payment AGENTS.md also change?}
B -- Yes --> C[Proceed with normal review]
B -- No, but significant change* --> D[CI warning: review whether AGENTS.md needs updating]
D --> E[Reviewer decides: update needed or genuinely irrelevant]*“Significant” here can be defined by simple heuristics — for example changes to files whose names contain state, invariant, or public interfaces in application/.
# example simple CI step (pseudo)
- name: Check context file freshness
run: |
if git diff --name-only origin/main | grep -q "modules/payment/application/"; then
if ! git diff --name-only origin/main | grep -q "modules/payment/AGENTS.md"; then
echo "::warning::modules/payment/application/ changed but AGENTS.md was not updated. Please review."
fi
fi
This is deliberately a warning, not a hard block — because not every change in that folder automatically needs a context file update. But the warning is enough to prompt the reviewer (human or agent) to consciously check.
Instructions to the Agent in the Root AGENTS.md
## Keeping Context Files Accurate
If your change adds/removes public interfaces in application/,
changes business invariants, or adds a new dependency to another
module, update the relevant module's AGENTS.md as part of the task —
before marking the work done.
The combination of explicit instructions (advisory) and CI warnings (safety net) provides layered assurance — similar to the pattern used for API contract synchronization in multi-service, only now executed within the same repository.
Anti-Patterns to Avoid
// ✗ One giant AGENTS.md at the root containing all modules' details
// -- every agent session carries noise from irrelevant modules
// ✓ Root for global conventions only, domain details in per-module files
// ✗ Context files listing the entire file tree manually
// -- goes stale quickly when files are added/removed
// ✓ Directory-purpose lines that are stable against individual file changes
// ✗ Shared domain models without explicit ownership
// -- anyone changes structs without realizing cross-module impact
// ✓ Every shared entity has a clear "owned by module X"
// ✗ CLAUDE.md containing a full copy of AGENTS.md (two sources of truth)
// -- easily goes out of sync when one of them is updated
// ✓ CLAUDE.md is just one @AGENTS.md line, or a symlink
Monolith Context File Review Checklist
STRUCTURE:
□ Root AGENTS.md contains only global conventions, not per-domain details
□ Every module with different domain rules has its own AGENTS.md
□ Generic/small folders DON'T have unnecessary separate AGENTS.md files
□ CLAUDE.md at every level is just an import (@AGENTS.md) or symlink
CONTENT:
□ Business invariants and specific gotchas live at the module level, not root
□ Directory-purpose lines are used, not manual full file trees
□ Shared domain models have explicit per-entity ownership
□ No content duplication between root and module
SYNCHRONIZATION:
□ There are explicit agent instructions about updating context when tasks finish
□ There's a CI check/warning detecting significant changes without
context file updates
□ The root file is reviewed periodically — is it still lean or starting
to bloat
Summary
- Multi-module monoliths still need context engineering — full technical access to all code doesn’t eliminate context window and cross-domain noise problems.
- Three layers: root (global conventions, rarely changes), module (specific domain rules), local (very specific details like external gateway integration).
- Root AGENTS.md for stack, module structure in general, and cross-module rules — not one domain’s business details.
- Module AGENTS.md for that domain’s invariants, boundaries, and dependencies — content style differs between modules (payment focuses on idempotency, catalog focuses on performance).
- CLAUDE.md at every level is just one
@AGENTS.mdline or a symlink — so Claude Code’s native lazy-load mechanism gets triggered without duplicating the source of truth.- Directory-purpose lines, not full file trees — explain each folder’s function in one line, resilient to individual file changes.
- Shared domain models must have explicit per-entity ownership — the most common breaking change source if ignored.
- Synchronization needs a combination of explicit agent instructions and CI warnings as a safety net, not relying on only one of them.