AI Context Engineering for Multi-Service Architectures
When you develop a service with the help of an AI coding agent, a new problem appears as soon as that service depends on another service: the agent knows nothing about the API, payloads, or events of its neighboring services. The easiest solution — cloning all repositories into one workspace, or pasting the entire API documentation into context — quickly becomes unscalable as the number of services grows. This article discusses how to give agents awareness of inter-service communication efficiently: the agent knows where to look for contracts, but only reads the details when actually needed. This approach applies to any team developing multi-service architectures with AI agent assistance, whatever tool or stack is used.
The Problem: Why “Open All Repositories” Doesn’t Scale
The most naive approach to giving an agent cross-service awareness is opening all related repositories into one workspace, then hoping the agent reads everything before working. This has several fundamental problems.
First, the context window fills with code irrelevant to the task. An agent that should focus on changing one endpoint in checkout-service instead has to process thousands of lines of code from payment-service, inventory-service, and notification-service at once — most of it completely irrelevant to the change being worked on.
Second, this violates team ownership boundaries. In organizations with many teams each owning their own services, opening another team’s entire source code into another developer’s workspace isn’t just about noise — it’s also about access and responsibility. The payment team may not want their internal source code to become part of the context generated by another team’s agent.
Third, contracts copied manually into documentation or context quickly become stale. Once another service changes, who is responsible for updating that copy? Usually nobody, until one day the agent makes a wrong assumption based on a contract that’s no longer valid.
flowchart TD
A[Agent working on checkout-service] --> B{Needs payment-service API knowledge}
B -- Naive approach --> C[Clone the entire payment-service repo]
C --> D[Context full of irrelevant code]
D --> E[Expensive tokens, high noise, uncontrolled access]
B -- Pointer approach --> F[Read pointer file: contract locations]
F --> G[Fetch only the specific contract, when needed]
G --> H[Lean context, restricted access, always relevant]This problem isn’t new — it’s another version of the dependency awareness problem that has long existed in software engineering, only now its consequences directly affect the quality of code generated by agents.
Core Principle: Pointers, Not Payloads
The solution to the above problem is actually simple in concept: the agent doesn’t need to carry the full contents of other services’ contracts in its context from the start. What it needs is to know where those contracts are, and instructions on when to read them. This is called the pointer pattern — the agent stores a lightweight reference, then lazy loads the full contents only when the task being worked on genuinely needs them.
The analogy is similar to #include in C or import in modern programming languages. A header file doesn’t carry the entire implementation — it just declares what’s available. The compiler (or in this case, the agent) only fetches the full details when actually invoked. Compare this to the wrong approach: pasting the entire function contents into every file that calls it.
// ANTI-PATTERN: paste the entire payment-service OpenAPI spec into the
// system prompt / agent instructions, even when the task doesn't touch payment
[500 lines of YAML OpenAPI pasted directly]
// CORRECT: reference the contract location, the agent fetches it when needed
Dependency: payment-service
Contract: docs/contracts/payment-openapi.yaml
Read this file ONLY if the task touches payment integration.
This principle also aligns with how modern agents work — they have tools to read files or call resources on demand. What you need to provide isn’t the full payload up front, but a clear path so the agent knows when and where to fetch that additional information.
- This pointer pattern isn’t just about saving tokens — it’s also about access security. The agent only needs to know the location of public contracts (API specs), not the full source code of other services.
- The less information forced into context at the start, the more the agent can focus on the task at hand.
Pointer File Anatomy
A pointer file is a small document, usually placed at the root of each service, that lists other services it interacts with along with their contract locations. The format can be YAML, JSON, or structured markdown — what matters is consistency across the organization.
# dependencies.yaml — placed at the root of checkout-service
consumes:
- service: payment-service
type: rest
contract: https://git.internal/payment-service/blob/main/openapi.yaml
endpoints_used:
- POST /v1/charge
- GET /v1/charge/{id}
last_verified: "2026-07-10"
- service: inventory-service
type: event
contract: https://git.internal/inventory-service/blob/main/schemas/order-events.avsc
events_consumed:
- order.reserved
- order.reservation_failed
produces:
- event: order.created
schema: schemas/order-created.json
known_consumers:
- inventory-service
- notification-service
Fields that should always be present:
| Field | Function |
|---|---|
service | Name of the dependency service |
type | Communication type — REST, gRPC, event, message queue |
contract | Location of the full contract — local path, git URL, or MCP endpoint |
endpoints_used / events_consumed | The specific parts of the contract actually used, not the entire API |
last_verified | When this contract was last checked as still valid |
The endpoints_used field matters because it narrows what the agent needs to read — you don’t need the entire payment-service OpenAPI spec, just the two endpoints checkout-service actually uses.
The “Read Only When Needed” Mechanism
Once the pointer file exists, the next question is how the agent technically reads the contract contents only when needed, not automatically in every session. There are several mechanisms, from the simplest to the most dynamic.
Local Files with Explicit Instructions
The simplest way: contracts are stored as files in the repo (for example docs/contracts/), and the instructions to the agent (AGENTS.md, system prompt, or task description) explicitly say “read file X if you’re changing code related to Y”. Modern agents like Claude Code, Cursor, or Codex CLI have built-in file-reading tools, so they can fetch these files themselves when the instructions are clear.
## Service Dependencies
This service integrates with payment-service (see dependencies.yaml).
Before modifying any payment integration code, read the referenced
contract file first. Do not assume payload structure from memory.
The weakness of this approach: contracts must be committed to the same repo (or accessible via a local path), which means a manual synchronization process is needed if the original contract lives in another service’s repository.
Git Sparse-Checkout or Contract-Only Submodules
If contracts (for example OpenAPI or proto files) live in each service’s own repository, you can use git sparse-checkout or submodules that only pull the contract folder — not the service’s entire source code.
# Sparse-checkout only the contracts/ folder from payment-service
git clone --filter=blob:none --sparse https://git.internal/payment-service.git
cd payment-service
git sparse-checkout set contracts/
With this, the agent (or the pipeline preparing context for the agent) can have access to the latest contracts without cloning another service’s full implementation. This is a good middle ground between “no access at all” and “full access to all code”.
MCP Resource Server
For more dynamic needs — where contracts change often and you want the agent to always get the latest version without clone or manual sync processes — each service can expose a small MCP (Model Context Protocol) server with resources like get_contract, get_endpoints, or get_recent_changes.
sequenceDiagram
participant Agent
participant MCP as MCP Server payment-service
participant Repo as payment-service repo
Agent->>MCP: get_contract("payment-service")
MCP->>Repo: read latest openapi.yaml
Repo-->>MCP: current contract
MCP-->>Agent: contract + version metadataWith this pattern, the agent never stores a contract copy that can go stale — every time it needs one, it queries the source directly. This is the most scalable approach for organizations with many services and teams that frequently ship changes.
RAG over a Contract Registry
When the number of services is already very large (dozens to hundreds), maintaining manual per-service pointer approaches becomes difficult to manage one by one. At this scale, index all contracts into a vector store (from OpenAPI specs, AsyncAPI, or proto files), and let the agent retrieve based on task relevance — not a predefined pointer list that must be manually maintained.
This approach is more complex to set up, but reduces the maintenance burden of maintaining pointer files one by one when the organization is already large.
Approach Comparison
| Approach | Setup Complexity | Real-time-ness | Best Fit Scale | Token Overhead |
|---|---|---|---|---|
| Local files + instructions | Low | Low (needs manual sync) | 1–10 services | Low |
| Git sparse-checkout | Medium | Medium | 10–30 services | Low–Medium |
| MCP resource server | Medium–High | High (always live) | 10–100+ services | Low (on-demand fetch) |
| RAG over registry | High | Medium–High | 50+ services | Medium (depends on retrieval) |
Decision Tree — Pick the Right Mechanism
flowchart TD
A{How many services?} -- 1-10 --> B{Do contracts change often?}
A -- 10-50 --> C{Need real-time?}
A -- 50+ --> D[Consider RAG over a contract registry]
B -- Rarely --> E[Local files + explicit instructions]
B -- Often --> F[MCP resource server]
C -- Yes --> F
C -- No --> G[Git sparse-checkout / contract submodules]End-to-End Implementation Example
Imagine a simple directory structure for checkout-service that depends on payment-service and inventory-service:
checkout-service/
├── AGENTS.md
├── dependencies.yaml
├── docs/
│ └── contracts/
│ ├── payment-openapi.yaml # sparse-checkout result, auto-updated
│ └── order-events.avsc
├── src/
│ └── ...
└── ...
The contents of AGENTS.md reference the pointer file instead of pasting its contents:
## Service Dependencies
This service depends on payment-service and inventory-service.
See dependencies.yaml for the full list and contract locations.
Rules:
- Before touching any code under src/integrations/payment/, read
docs/contracts/payment-openapi.yaml first.
- Do not assume payload shapes from memory or from old code comments.
- If your change adds a new dependency or modifies an existing
integration, update dependencies.yaml as part of the task (see
"Keeping Pointer Files in Sync" below).
The flow becomes: the agent starts a task, reads AGENTS.md, sees the dependency instructions. If the task does touch payment integration, the agent then opens docs/contracts/payment-openapi.yaml — not the entire payment-service source code. If the task doesn’t touch any integration, this pointer file is never even read, so it consumes no context at all.
Special Tip: Making Claude Code Read AGENTS.md
One technical thing to note if you use Claude Code: natively, Claude Code reads the file CLAUDE.md, not AGENTS.md. If your team uses several AI coding agents at once (Claude Code, Cursor, Codex CLI) and wants one shared source of truth, there are two ways to make Claude Code still read AGENTS.md:
// CLAUDE.md — first line imports AGENTS.md
@AGENTS.md
// Additional Claude Code-specific instructions can go below
Alternatively, run the /init command in a repo that already has AGENTS.md — Claude Code will automatically read and merge its contents (including .cursorrules and .windsurfrules if present), without you needing to write CLAUDE.md manually.
- If the team uses only one tool, use that tool’s native format directly (
CLAUDE.mdfor Claude Code only).- If the team uses more than one AI coding agent, put shared instructions in
AGENTS.md, then create each tool’s native file (CLAUDE.md,.cursorrules) that imports from it — so there’s no duplication to maintain manually.
Keeping Pointer Files in Sync — Agent-Driven Updates
A stale pointer file is as dangerous as having no pointer file at all — even worse, because it creates false confidence. An agent reading an outdated contract produces code that looks right but is actually based on wrong assumptions. This section discusses how to ensure pointer files and contracts stay in sync when dependencies change.
Triggers: When Updates Need to Happen
Several conditions should trigger an update to the pointer file or contract:
- A new endpoint is added or removed
- Payload/schema changes — new fields, changed data types, removed fields
- Paths or routes change
- A new dependency appears — service A starts calling service C which it never called before
- New events are published or consumed, for event-driven architectures
Approach 1 — Self-Update as Part of the Agent’s Task Checklist
The most direct way: explicitly instruct in AGENTS.md that every change impacting communication contracts must include a pointer file update, as part of the definition of “task done” — not an optional step.
// ANTI-PATTERN: the agent changes an endpoint, but dependencies.yaml
// is never touched — the contract goes stale unnoticed
// CORRECT: explicit instruction in AGENTS.md
If your change adds, removes, or modifies endpoints/payloads/
events used by other services, update dependencies.yaml and the
related contract files BEFORE marking the task done. This is part
of the Definition of Done, not an optional extra step.
Instructions like this are advisory, not mechanically enforced — the agent can still forget. That’s why this approach ideally should be combined with the safety net in Approach 3.
Approach 2 — Generate Contracts from Code, Not Written Manually
Instead of relying on the agent (or a human) to remember to update a separate contract document, contracts can be generated automatically from the code itself — for example an OpenAPI spec from route annotations, or a proto file that becomes the single source of truth for gRPC.
// ANTI-PATTERN
openapi.yaml written manually, separate from route code
-> easily goes out of sync when routes change
// CORRECT
openapi.yaml generated from annotations/decorators in the code
-> the agent just changes code, the generator keeps the contract accurate
With this pattern, “update the contract” is no longer a separate manual step that can be forgotten — it automatically follows code changes. The trade-off is needing tooling setup up front, and not every stack has mature generators for every contract type (REST is relatively easy, event schemas are sometimes trickier).
Approach 3 — CI/Git Hooks as a Safety Net
For cases where Approach 1 fails (the agent or a human forgets to update manually), the CI pipeline can detect changes to route/schema files and validate whether the related contract has been updated.
flowchart TD
A[Agent commits code changes] --> B{CI detects route/schema diff?}
B -- No --> C[Proceed with merge as usual]
B -- Yes --> D{Related contract also updated?}
D -- Yes --> C
D -- No --> E[Block merge / auto-regenerate the contract]
E --> F[Notify on the PR: update the contract before merging]This safety net matters because instructions to agents are advisory in nature — a CI check that genuinely blocks merges is a stronger guarantee than merely hoping the agent obeys instructions.
Approach 4 — Notify Consumer Services
One thing often overlooked: after a producer’s contract changes, how does the consumer service know? Updating the pointer file on the producer side alone isn’t enough if the consumer is never told a change happened.
Some options:
- Per-contract changelog — a
CHANGELOG.mdfile in the contracts folder recording every change with dates - Schema versioning — semantic versioning on schemas, so breaking changes are clearly visible from a major version bump
- Active notification — webhooks or messages to consumer team channels whenever a producer contract changes
- MCP tool
get_recent_changes— if using an MCP resource server, provide a tool returning contract diffs since a given timestamp, so agents in later sessions can check “did anything change since I last read?”
Synchronization Approach Comparison
| Approach | Needs Instruction Discipline | Miss Risk | Setup Complexity |
|---|---|---|---|
| Manual self-update | High | High (depends on agent compliance) | Low |
| Generate from code | Low | Low | Medium–High |
| CI safety net | Low (automatic) | Low | Medium |
| Consumer notification | Medium | Medium | Low–Medium |
- An agent that “forgets” to update a contract is more dangerous than having no contract at all — a wrong contract creates false confidence in later agent sessions, instead of making the agent ask or double-check.
- Don’t rely only on AGENTS.md instructions for contract synchronization. Combine them with a CI check (Approach 3) as a mechanical guarantee, not just advisory.
When This Pattern Isn’t Needed
Not every team needs all the mechanisms above. The pointer and automated synchronization patterns have setup overhead that should be proportionate to their benefits.
NEED this pattern if:
✓ More than 3-4 interdependent services
✓ Separate team/service ownership (not the same team)
✓ Contracts between services change fairly often
✓ Incidents have already happened from agents assuming contracts wrongly
DON'T NEED if:
✗ A small monorepo with 1-2 services, the same team
✗ Contracts almost never change
✗ All developers (and agents) already have full access to all code without ownership issues
For small teams with monorepos, sometimes the most efficient solution is still opening all the code into one workspace — the overhead of building pointer files and MCP servers isn’t worth the benefit at that scale.
Summary
- The core problem: opening all other services’ repositories into one workspace doesn’t scale — it causes context bloat, violates team ownership boundaries, and manually copied contracts go stale quickly.
- The core principle: use the pointer pattern — the agent knows where contracts are, rather than carrying their full contents from the start. Read only when the task genuinely needs them.
- A pointer file only needs to contain the service name, communication type, contract location, and the specific parts used — not the entire API specification.
- Four mechanisms from simple to complex: local files + explicit instructions, git sparse-checkout/submodules, MCP resource servers for real-time, and RAG over a contract registry for large scale.
- Claude Code reads
CLAUDE.md, notAGENTS.mdnatively — use@AGENTS.mdas an import line inCLAUDE.md, or run/initin a repo that already hasAGENTS.md.- Contract synchronization must not rely on manual instructions alone — combine agent self-update, generating contracts from code, a CI safety net, and notification to consumer services.
- A stale contract is more dangerous than having no contract at all, because it creates false confidence in the agent in later sessions.
- This pattern isn’t always necessary — for small monorepos with one team, the setup overhead may not be worth the benefit.