In March 2023, Amazon's Prime Video team moved one of its monitoring tools off a microservices setup and back into a single application, which cut its running cost by around 90 percent. The write-up spread widely because it put numbers on something a lot of teams had started to feel, that they had split too far.
Since then many teams have folded services back into larger units, and AI coding agents are pushing in the same direction, because an agent works better in a codebase it can read all at once than across a dozen services wired together over the network. For most teams the practical question now is how few services they can get away with, and whether a given split is worth what it costs to run. It helps to treat the options as a spectrum rather than two camps, since most teams end up somewhere in the middle.
The architecture spectrum: a single monolith, a modular monolith with firm internal boundaries, and fully separate microservices.
Microservices became the default answer in the 2010s, promoted as the way large engineering orgs like Netflix and Amazon scaled. Most teams adopting them were nowhere near that scale, so they took on the operating cost without having the problem it solves, and often ended up with a distributed monolith: services coupled tightly enough that they had to deploy together anyway, with network calls added in between for no real gain. Over the last couple of years that has been unwinding, and surveys through 2025 report a large share of teams merging services back into fewer, larger units.
| Dimension | Monolith | Microservices |
|---|---|---|
| Deployment | One unit | Independent per service |
| Scaling | Whole app together | Per service |
| Team workflow | Shared codebase and deploy | Teams own services independently |
| Operational complexity | Low | High (networking, discovery, observability) |
| Local development | Run one app | Run or mock several services |
| Failure isolation | A crash can take down the app | Failures can be contained per service |
| AI agent context | Whole system visible in one codebase | Context split across service boundaries |
| Data | Usually one database | Often a database per service |
| Best fit | Most new projects, small to mid teams | Large systems, many teams, uneven scaling |
An AI coding agent works by reading your codebase and reasoning over what it can see. Take a change to how an order gets priced. In a monolith, the agent reads the request handler, the pricing rules, and the database schema together and updates them in one pass. Split the system into an orders service, a pricing service, and a catalog service, and the pricing rules sit behind an HTTP boundary the agent sees only as a function signature, the schema lives in another repository, and it has to guess at the parts it cannot read. The same change turns into several coordinated edits across services, and coordination is where both agents and people drop things.
As agents write more of the code, keeping a system in one navigable codebase has become a real advantage, which is a large part of why teams now reach for a modular monolith on new work.
Start with a monolith and tighten it into a modular monolith as the codebase and team grow. A modular monolith is still one deployable unit, but with firm boundaries between modules and clear interfaces between them, so you get the structure of separate services without the network calls or a deploy pipeline per service. Shopify runs a large modular Rails codebase this way, serving an enormous business without splitting into hundreds of services. It stays one thing to run, and any module that later needs to stand alone already has a boundary to extract along.
Extract a service only when you can name why. The signals that justify it:
If you cannot name the first service you would pull out and the reason for it, wait. Splitting without one brings the operational cost of microservices and none of the benefit.
A decision flow: start as a monolith, tighten into a modular monolith as you grow, and extract a service only when you can name a concrete reason such as scaling, deploy contention, or isolation.
Most of the monolith-versus-microservices tension is operational: microservices give you independent services and clear boundaries, and charge you the running cost to get them. Encore removes most of that cost. You write your services in one codebase and call between them as ordinary functions, type-checked at compile time, so the system reads like a monolith.
// orders/orders.ts
import { api } from "encore.dev/api";
import { users } from "~encore/clients";
import { products } from "~encore/clients";
export const create = api(
{ method: "POST", path: "/orders", expose: true },
async (p: CreateParams): Promise<Order> => {
// Calling another service is a type-checked function call.
// No HTTP client, no service URL, no manual serialization.
const user = await users.get({ id: p.userId });
const product = await products.get({ id: p.productId });
return placeOrder(user, product, p.quantity);
},
);
Each service still gets its own database, queues, and whatever else it needs, provisioned into your own AWS or GCP account with sensible defaults you can adjust, and service discovery and distributed tracing are wired up with the whole thing deployed together. You keep the boundaries that push people toward microservices, along with the single navigable codebase that makes a monolith easy to work in and easy for an agent to reason about. The TypeScript and Go walkthroughs build a multi-service backend this way from scratch.
For most new projects in 2026, start with a monolith or a modular monolith. A single codebase is simpler to build, test, and operate, and it stays productive well past early growth. Split a piece into its own service when it has a concrete reason to be separate, like a very different scaling need or a team that keeps blocking on a shared deploy. Splitting earlier than that adds operational work without a matching payoff.
AI coding agents generally reason better over a monolith or modular monolith, because they can see the whole codebase in one place, including call paths, data models, and side effects. Spreading the system across many services hides much of that context behind network boundaries, so the agent works with less and makes more mistakes. This is one reason teams doing agent-assisted development lean toward keeping services few and the codebase navigable.
Move when you can name a concrete reason. One part of the system needs to scale differently from the rest, teams are blocked waiting on a shared deploy, or a component needs isolation for reliability or compliance. If you cannot say which service you would extract first and why, it is usually too early to split.
A modular monolith is a single deployable unit with firm boundaries between its internal modules. The code is organized like separate services, with clear ownership and interfaces, but it runs as one app. You get most of the structure of microservices without the network calls and separate deploys, and you can extract a module into its own service later if a real need appears. It is becoming the default choice for new systems in 2026.
Usually not at first. A small team ships more with a monolith or modular monolith, since there is one codebase to run and deploy. Microservices add network calls, separate deploys, and distributed debugging, which cost time a small team rarely has to spare. They become worth it as the team and the system grow past what one codebase can hold comfortably.
Partly. Some frameworks handle the plumbing for you. Encore, for example, lets you write several services in one codebase with type-checked calls between them, handles service discovery, and provisions each service's infrastructure into your own AWS or GCP account. You get service boundaries without running a Kubernetes cluster, though a large system will still involve real distributed-systems work like data consistency across services.