How context limits, service boundaries and the ability to run the whole system locally change the choice when a coding agent writes much of the code.
A coding agent reads the files it thinks are relevant, edits them, runs a check and reads the result, all inside a context window that has a fixed size. Anthropic's engineering team describes context as a finite resource with diminishing returns, and Chroma's context rot report found that models become less reliable as input length grows, even on simple tasks.
Architecture decides how much the agent has to read for a given change and whether it can run what it changed, which is why the monolith and microservices question looks different when an agent does much of the work.
A monolith keeps everything readable in one place and runs with one command, but it gets harder to work in when the codebase is large enough that the agent reads the wrong half of it, or when two teams step on each other's deploys. Microservices give the agent smaller units to work in and get harder when a change crosses a boundary, because the other service is in a different repository, the contract is an OpenAPI file that may be out of date, and the only way to test the two together is a shared staging environment. The general tradeoffs are covered in monolith vs microservices; this article looks at the parts that matter for agents.
What to weigh when choosing:
Encore combines the boundaries of microservices with the local workflow of a monolith: an app is one repository where every directory with a service definition is a service, each service declares its own databases, Pub/Sub topics and buckets in code, and calls between services are typed function calls that Encore turns into network calls. encore run starts all services locally with their infrastructure, so an agent that changes the orders service and the billing service in one task can call the endpoint and watch the request cross both, and a deploy provisions the services and their resources in your own AWS or GCP account.

In TypeScript, a service is a directory with an encore.service.ts file, and its endpoints and database live next to it:
// billing/encore.service.ts
import { Service } from "encore.dev/service";
export default new Service("billing");
The billing service exposes an endpoint with typed request and response shapes, which is the contract other services compile against:
// billing/billing.ts
import { api } from "encore.dev/api";
interface ChargeRequest {
orderId: string;
amountCents: number;
}
interface ChargeResponse {
chargeId: string;
}
export const charge = api(
{ method: "POST", path: "/billing/charge" },
async (req: ChargeRequest): Promise<ChargeResponse> => {
return { chargeId: `ch_${req.orderId}` };
},
);
The orders service calls it through the generated client in ~encore/clients, so renaming a field in ChargeRequest is a type error in the orders service rather than a failed request in staging:
// orders/orders.ts
import { api } from "encore.dev/api";
import { billing } from "~encore/clients";
export const checkout = api(
{ expose: true, method: "POST", path: "/orders/:id/checkout" },
async ({ id }: { id: string }): Promise<{ chargeId: string }> => {
const { chargeId } = await billing.charge({ orderId: id, amountCents: 4200 });
return { chargeId };
},
);
Because Encore knows every service and every call between them from static analysis, the local development dashboard shows a service catalog and distributed traces across services, and the local MCP server exposes the same metadata and traces to the agent, so it can check which services a request touched. Python and Rust services follow the same model with an encore.service.json per service, and services in different languages can live in one app, as Python microservices shows.
encore run starts every service locally with its databases, topics and buckets.Splitting a service is a matter of moving code into a new directory with its own service definition, and the typed clients make every broken call site visible. Service clients cannot be passed around freely because the calls are found through static analysis, and the .ref() method on a client exists for those cases.
A single codebase that runs as one process is the easiest architecture for an agent to work in at small and medium size, since every caller of a function is a search away and a refactor is checked by one build, and it gets harder as the codebase grows, because the agent has more to search and more unrelated code competes for its context, and Claude Code's documentation notes that more specific and concise instructions are followed more consistently. Splitting instructions per directory helps, since CLAUDE.md files in subdirectories load only when the agent works there.
A modular monolith keeps one process and adds boundaries the build checks, which gives the agent smaller areas to reason about without a network in between. In Python, import-linter turns the boundaries into a contract that fails CI when a module reaches into another's internals:
[importlinter]
root_package = shop
[importlinter:contract:billing-boundary]
name = Orders only uses the billing public API
type = forbidden
source_modules =
shop.orders
forbidden_modules =
shop.billing.internal
The boundaries are only as strong as the checks around them, so the contract file and the command that runs it belong in AGENTS.md. Microservices vs modular monolith covers how to keep these boundaries from eroding.
Putting every service in one repository gives the agent the whole system to search, and a Docker Compose file can start the services and their dependencies together:
# compose.yaml
services:
orders:
build: ./orders
environment:
BILLING_URL: http://billing:8080
DATABASE_URL: postgres://postgres:postgres@orders-db:5432/orders
depends_on: [billing, orders-db]
billing:
build: ./billing
orders-db:
image: postgres:17
environment:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: orders
The Compose file is a second description of the system that drifts from the code, every new service or database means editing it, and the HTTP contract between services is only typed if the team generates clients from a schema. Tracing a request across services locally needs OpenTelemetry wired into each one.
One repository per service gives teams full independence but is the hardest setup for an agent, because the other services are visible only through their API documentation, a change to a contract means coordinated pull requests in several repositories, and running the full system usually means deploying to a shared environment. Claude Code can run subagents, each in its own context window, which helps split work, but each one still needs access to the code it changes and a way to run it.
A monolith or a modular monolith is usually easier for an agent, because it can read the related code in one repository and run the whole app with one command. Microservices work well with agents when the services live in one repository, call each other through typed clients rather than hand-written HTTP, and can all run locally together, so the agent can test a change that crosses a service boundary before it is deployed.
Keep the services in one repository and use tooling that starts them together with their infrastructure. With Encore, every directory with a service definition is a service, and encore run starts all of them locally with their databases, Pub/Sub topics and buckets, with service-to-service calls traced so the agent can see the whole request. Without Encore, a Docker Compose file that starts each service and its dependencies does a similar job, but has to be kept in sync by hand.
They have to work within a finite context window, and research such as Chroma's 2025 context rot report found that model performance becomes less reliable as input length grows. Clear module or service boundaries help, because the agent can work on one part while reading only the interfaces of the others, as long as those interfaces are typed and in the same repository.
Deploy to your cloud on AWS and Google Cloud
without managing infrastructure
Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.