// Stay in touch?
Products
Encore CloudEncore Cloud
Encore.tsEncore.ts
Encore.goEncore.go
PricingPricing
Book a DemoBook a Demo
Use Cases
AI-Powered DevelopmentAI-Powered Development
Event-Driven SystemsEvent-Driven Systems
Distributed SystemsDistributed Systems
Case StudiesCase Studies
ShowcaseShowcase
Resources
DocsDocs
InstallInstall
Example AppsExample Apps
Demo videoDemo video
ArticlesArticles
GitHub ReleasesGitHub Releases
Systems Operational
Company
About UsAbout Us
Swag ShopSwag Shop
ContactContact
JobsJobs
PressPress
SecuritySecurity
TermsTerms
Privacy PolicyPrivacy Policy
Data Processing AgreementData Processing Agreement
Enterprise SLAEnterprise SLA
Encore
© 2026 EncoreAll rights reserved
© 2026 Encore All Rights Reserved
GitHubDiscordYouTube

Microservices in a Monorepo

How to structure many services in one repository, share code without hidden coupling, and decide between monorepo and polyrepo

07/20/26
9 Min Read
Ivan Cernja
07/20/26

Microservices in a Monorepo

How to structure many services in one repository, share code without hidden coupling, and decide between monorepo and polyrepo

Ivan Cernja
9 Min Read

Google keeps most of its code in a single repository holding billions of lines and thousands of services, and a change that touches many of them lands as one commit reviewed in one place. Most teams never reach that scale, but they can borrow the shape, since a monorepo holds many independently deployed services in one repository regardless of size. Interest in the pattern has climbed over the last few years as teams that split into microservices found the polyrepo sprawl, one repository per service, harder to change than the services themselves.

A monorepo describes where code lives, and it says nothing about how the system runs, so you can hold a dozen services in one repository and still deploy each on its own. What decides whether that works is how you lay the repository out, how you share code between services without quietly welding them together, and whether a monorepo is the right call over a repository per service.

A monorepo layout: a services directory holding several independent services, and a packages directory holding shared internal packages that the services import.

one repositoryservices + shared packages
services/
service
orders
service
users
service
billing
packages/
shared
types
shared
auth
shared
db-client
Services stay separate; shared packages sit beside them, imported not copied.

A layout that scales

Most monorepos settle into two top-level directories, one for services and one for shared packages. Each service owns its directory completely, including its handlers, its database migrations, and its deploy configuration, so a person or an agent working on orders has everything that service needs in one place. Shared code lives under packages, split into small units by concern rather than one grab-bag utility package that every service ends up depending on.

repo/ services/ orders/ # handlers, migrations, deploy config users/ billing/ packages/ types/ # shared domain types auth/ # auth helpers db-client/ # thin database wrapper package.json # workspace root turbo.json # or nx.json, for build orchestration

The workspace root ties it together. In a TypeScript repository that is npm, pnpm, or Yarn workspaces declaring where the packages live, usually paired with a build tool like Turborepo or Nx that understands the dependency graph between services and packages. In a Go repository it is a single module or a Go workspace with go.work. Either way the tooling knows that orders imports types, so when types changes it knows orders needs rebuilding and users might not.

Sharing code without hidden coupling

The reason to keep services in one repository is that sharing becomes trivial, and that is also the trap. A shared package is imported by every service that depends on it, so a change to that package is a change to all of them at once. This is fine when the package is small and its interface is stable, and it turns into distributed coupling when the package grows into a shared core that half the services reach into.

A few rules keep the sharing honest:

  • Share types and contracts freely, share behavior carefully. A package of shared domain types is cheap to depend on and catches mismatches at compile time. A shared package full of business logic pulls that logic into every caller, and now a change for one service ripples through the rest.
  • Never import another service's internal files. If orders needs something from users, it should go through the interface users publishes, not reach into its handlers or its database. A direct import across service internals is the coupling microservices exist to prevent, and a monorepo makes it one autocomplete away, so it needs a rule.
  • Watch the shared database. The fastest way to weld two services together is to let both read and write the same tables. The schema becomes a contract neither service owns, and a migration for one breaks the other. Give each service its own data and let them talk through interfaces.
  • Keep shared packages small and stable. The bigger a shared package and the faster it changes, the more it behaves like a single point that couples everything downstream. Treat each one as a public interface with a contract you are reluctant to break.

What these rules protect is the difference between coupling you can see and coupling you cannot, because an explicit import through a published interface shows up in review and gets checked by the compiler, while a shared mutable data model or a deep reach into another service's guts stays hidden until a change in one place breaks something in another. A monorepo makes that hidden coupling easy to create by accident, so the rules exist to keep the dependencies you take on visible.

One pipeline or one per service

Sharing a repository does not mean sharing a deploy, and getting this wrong is how teams end up with the worst of both worlds. If every commit rebuilds and redeploys every service, you have a monolith release cadence with microservice operational cost, which is the distributed monolith under a different name.

The setup that works detects which services a commit touched and acts only on those. Build tools like Turborepo, Nx, and Bazel model the dependency graph, so a change to the orders directory rebuilds orders, a change to the shared types package rebuilds every service that imports it, and a change to a service nobody depends on rebuilds nothing else. CI runs the affected tests and deploys the affected services, and the rest stay untouched.

A monorepo CI pipeline: one commit is analyzed against the dependency graph to find affected services, then only those services are built, tested, and deployed independently.

one commitgit · CI trigger
ci
Find affected services
affected only
step
Build
step
Test
step
Deploy
One repository, one commit, but only the affected services rebuild and ship.

The payoff is a change that crosses services, say adding a field that orders sends and billing reads, arriving as one commit, one review, and one atomic merge. In a polyrepo that same change is two pull requests in two repositories that have to merge and deploy in the right order, and the window between them is where things break. The cost is that CI has to be graph-aware from early on, because a naive pipeline that runs everything on every commit gets slow and expensive fast as the repository grows.

Monorepo or polyrepo

The choice is mostly about who owns what and how often changes cross service lines.

DimensionMonorepoPolyrepo
Cross-service changeOne commit, one reviewCoordinated across repos
Code sharingDirect import of internal packagesPublished versioned packages
Team isolationSofter, enforced by conventionHard, enforced by repo boundaries
Access controlUsually repo-widePer repository
Release cadenceIndependent, if CI is graph-awareIndependent by default
Tooling investmentHigher up front (build graph, CI)Lower to start, sprawls later
Best fitOne team or a few, changes cross servicesIndependent teams, strict ownership

A monorepo fits when a single team or a handful of them own most of the services and changes routinely cross boundaries, because keeping everything in one place makes those changes cheap to make and easy to review together. It also gives an AI coding agent the whole system to read at once, which matters more as agents write more of the code and reason better over a codebase they can see end to end than across repositories they cannot.

A polyrepo fits when services are owned by independent teams that need hard isolation, their own release schedule, and access control scoped per service, since a repository boundary enforces all three without relying on convention. The honest tradeoff is that a monorepo trades enforced isolation for cheap coordination, so it asks for discipline and tooling in exchange for making cross-service work easy. For most teams below the scale where separate teams truly own separate services, the monorepo is the better starting point, and a service can always be split into its own repository later when a team needs to own it end to end.

Where Encore fits

A monorepo takes work to assemble: a workspace, a build tool that understands the dependency graph, a CI pipeline that deploys only affected services, and the discipline to keep shared code from turning into hidden coupling. An Encore app arrives as a monorepo by construction. You define several services in one codebase, and they call each other through the ~encore/clients import as ordinary typed functions, so the shared contract between services is the function signature itself, checked at compile time.

// orders/orders.ts import { api } from "encore.dev/api"; import { users } from "~encore/clients"; export const create = api( { method: "POST", path: "/orders", expose: true }, async (p: CreateParams): Promise<Order> => { // A call into another service in the same repo, // type-checked at compile time. No shared table, // no reaching into the users service internals. const user = await users.get({ id: p.userId }); return placeOrder(user, p.quantity); }, );

Because the calls are type-checked, a breaking change to a service's interface surfaces across every caller in the repository at compile time rather than in production, which is the exact coupling that a hand-built monorepo has to guard against by convention. Each service still gets its own database and infrastructure, declared in code and provisioned into your own AWS or GCP account, and services deploy together from the one codebase. The TypeScript walkthrough builds a multi-service backend this way from scratch, and the monolith vs microservices guide covers the broader tradeoff a monorepo layout sits inside.

Related reading

  • Monolith vs Microservices: How to Choose in 2026
  • How to Build Microservices with TypeScript
  • Microservices Best Practices
  • Service-to-Service Communication

Frequently asked questions

Can microservices live in a monorepo?

Yes, and many teams run dozens of services from one repository. Each service keeps its own directory, deploy target, and often its own database, while shared types and utilities live in common packages the services import. Google, Meta, and Uber run large service estates this way. The monorepo holds the code together so changes across services land in one commit and one review, but each service still deploys on its own.

How do you share code between microservices in a monorepo?

Put shared code in versioned internal packages that services import, such as a package for common types, one for auth helpers, and one for a database client. Keep those packages small and stable, since anything they touch becomes coupling across every service that imports them. Avoid reaching directly into another service's internal files. Import through its published interface so the boundary stays visible and the two can still deploy separately.

Should each microservice have its own repository or share one?

It depends on team structure. A monorepo fits when a small or mid-sized team owns most services and changes often cross service boundaries, since one commit and one review cover the whole change. Separate repositories fit when independent teams need hard isolation, their own release cadence, and strict access control per service. Start with a monorepo and split a service out only when a team needs to own it end to end.

Does a monorepo mean one big deployment?

No. A monorepo is about where code lives, not how it ships. Services in a monorepo still build and deploy independently. A good CI setup detects which services a commit changed and rebuilds only those, so touching one service does not redeploy the rest. Sharing a repository does not force sharing a release, and treating it as one deploy recreates the distributed monolith you were trying to avoid.

What is the difference between a monorepo and a monolith?

A monolith is one deployable application that runs as a single unit. A monorepo is one repository that can hold many independently deployable services. You can have a monolith in its own repo, or many microservices in a single monorepo. The monorepo describes source layout; the monolith describes runtime shape. Confusing the two leads teams to think one repository means one process, which is not the case.

How do you stop shared code from coupling services together?

Keep shared packages minimal and treat them as public interfaces with a stable contract. Do not let services import each other's internal files, and be cautious about a shared data model that many services write to, since that quietly binds their schemas together. Some frameworks make the boundary type-checked, so a breaking change to a shared type surfaces at compile time across every caller rather than in production.

Contents
A layout that scales
Sharing code without hidden coupling
One pipeline or one per service
Monorepo or polyrepo
Where Encore fits
Related reading
Encore

The backend platform for humans and agents

Build backends in Go or TypeScript and run them on your own AWS or GCP, with the guardrails your team and AI agents need to ship safely.

Get started

Ready to build your next backend?

Encore is the Open Source framework for building robust type-safe distributed systems with declarative infrastructure.