Guide contents

Lesson 60 of 129

Monorepo, Polyrepo, and Shared Libraries

Repository layout and code sharing determine how independent services really are. Two decisions matter: whether services live in one repository (monorepo) or many (polyrepo), and how code common to several services is shared. Both can reintroduce the coupling that microservices are meant to remove.

Monorepo

A monorepo stores the code of many services (and libraries) in one version-control repository. This is a repository decision, not a deployment one: services are still built and deployed independently.

Advantages:

  • Atomic cross-service changes. One commit can change a producer, its consumers, and a shared schema, and CI tests them together.
  • Simple code sharing and discovery. Shared code is in the same tree; refactoring updates all usages at once.
  • Consistent tooling and dependency versions across services.
  • Single history and code search.

Costs:

  • Requires build tooling that builds and tests only what a change affects (dependency graph analysis, caching), such as Bazel, Buck, Pants, or Nx. Without it, CI time grows with repository size.
  • Access control is per repository by default, so per-service permissions need extra mechanisms (code owners files, path-based rules).
  • Very large repositories strain version control and editor tooling, and may need partial checkouts (git sparse checkout) or specialized systems.
  • A shared tree encourages cross-service source dependencies, which bypass the API contracts.

Polyrepo

A polyrepo setup has one repository per service (or per group of related components).

Advantages:

  • Clear ownership and access control: repository-level permissions match team boundaries.
  • Independent build, test, and release with small, fast pipelines.
  • Enforced boundaries: a service cannot import another's source, only its published API or artifacts.
  • Independent tooling and technology choices.

Costs:

  • Cross-service changes need several pull requests, coordinated across repositories.
  • Shared code needs publishing as versioned packages.
  • Dependency versions drift between repositories; consistent upgrades (for security patches) need automation.
  • Discovering code and its usages is harder.

Comparison

Aspect Monorepo Polyrepo
Cross-service change One commit Several coordinated PRs
Code sharing Direct, in-tree Published packages
Boundary enforcement Needs tooling and discipline Natural
CI Needs affected-target detection Simple per repository
Dependency versions Uniform Can drift
Access control Path-based mechanisms Per repository

Neither choice by itself makes services independent. A monorepo with strict module boundaries and independent pipelines can support independent deployment; a polyrepo with lock-step releases cannot.

Shared libraries

Shared libraries contain code used by several services. They reduce duplication but create compile-time coupling: a change to the library requires consumers to update and redeploy.

What is reasonable to share

  • Cross-cutting technical code with no business logic: logging, metrics and tracing instrumentation, HTTP or gRPC client setup, retry and circuit breaker helpers, authentication token validation.
  • Generated client code from an API definition.
  • Utilities with stable interfaces.

What is harmful to share

  • Domain models and business rules. A shared Customer class makes every service that uses it depend on one model, which contradicts the bounded-context principle that each context has its own model (see domain-driven design). A change requires all consumers to upgrade together, which is the distributed monolith pattern (see the distributed monolith).
  • Libraries that grow into a "common" module used for everything.
  • Persistence code and entity definitions that reveal a shared database schema.

Example: a shared library defines Customer and is used by 12 services. Adding a required field to it means 12 services must upgrade, test, and redeploy before the field can be relied on, and services that still run the old version cannot deserialize new data if the change is not backward compatible.

Duplicating a small amount of code between services is usually cheaper than coupling them. The cost of duplication is fixing a bug in several places; the cost of coupling is coordinated releases.

Versioning shared libraries

  • Use semantic versioning: a major version increment signals a breaking change.
  • Consumers pin versions and upgrade on their own schedule. Services must not require the newest version to run.
  • Keep the library backward compatible so that services on different versions can interoperate at runtime.
  • Automate dependency update pull requests, so old versions do not remain indefinitely.
  • Cross-cutting concerns that must behave identically across languages (mTLS, retries, timeouts) can be moved out of libraries into a sidecar proxy or platform layer (see service mesh and sidecars).

Sharing schemas and contracts

Services must agree on the shape of messages and APIs. The contract is shared; the implementation is not.

flowchart LR
    Schema["Contract: OpenAPI / Protobuf / Avro schema"] --> ProdGen["Producer: generated types"]
    Schema --> ConsGen["Consumer: generated types"]
    Schema --> Reg["Schema registry / contract repo"]
    Reg --> Check["CI compatibility check"]

Practices:

  • Define contracts in a language-neutral format: OpenAPI for REST, Protobuf for gRPC (see gRPC and Protobuf), Avro or Protobuf or JSON Schema for events.
  • Store contracts in a dedicated location (a contracts repository or directory, or a schema registry) with the owning service's team as owner.
  • Generate client and server types from the contract in each service's own language instead of sharing hand-written classes.
  • Run compatibility checks in CI, which reject changes that break existing consumers (removing a field, changing a type).
  • Use consumer-driven contract tests so that consumers state what they depend on (see API versioning and contract testing).
  • Consumers follow the tolerant reader principle: ignore unknown fields and read only what they need.

Interview questions

  • Does a monorepo mean a monolith? No. A monorepo is a repository layout. Services in it can still be built, tested, and deployed independently, given tooling that builds only affected components.
  • Why is a shared domain model library harmful? It couples every consumer to one model and one version, so a change forces coordinated upgrades of all services, and it contradicts separate models per bounded context.
  • What is reasonable to put in a shared library? Technical cross-cutting code without business logic, such as logging, tracing, client setup, and generated API clients, with semantic versioning and backward compatibility.
  • How are schemas shared without coupling services? Publish a language-neutral contract, generate types per service, check compatibility in CI, and use tolerant readers so that unknown fields do not break consumers.
  • What are the main costs of a polyrepo? Cross-service changes need multiple coordinated pull requests, and dependency versions and shared code drift unless updates are automated.