DEV Community

Joung Park
Joung Park

Posted on

Monorepo or Polyrepo?

After deciding to use microservices, the next question was:

How should I organise the codebase?

I chose a monorepo.

Second-Memory has several applications and services:

apps/
  web/
  mobile/

services/
  memory-service/
  ask-service/
  embedding-worker/

packages/
  api-types/
  ui-components/
Enter fullscreen mode Exit fullscreen mode

They are independently deployable, but they are all part of the same product.

Monorepo vs Polyrepo

There is no universally better choice.

The trade-offs become clearer when looking at common development tasks:

Area Monorepo Polyrepo
Share code Easy — reusable code can live in shared packages Harder — publish packages or duplicate code
Share types Easy — TypeScript types can be shared directly Harder — types need separate versioning
Cross-project changes Easy — related changes can be made in one PR Harder — changes may require multiple PRs
Dependency management Easy — dependencies can be managed consistently More complex — each repo manages its own dependencies
Local development Easy — one repository contains the whole system More setup — multiple repositories need to be configured
CI/CD More complex — need to detect affected projects Easy — each repository naturally has its own pipeline
Service isolation Harder — boundaries need to be enforced within the repository Easy — repositories provide natural isolation
Access control Harder — repository access generally covers the whole codebase Easy — permissions can be managed per repository
Independent releases Possible — CI/CD can deploy projects independently Easy — each repository has its own release lifecycle

Neither approach is inherently better.

For Second-Memory, I valued easy development across the whole system more than repository-level isolation.

When to Choose Which

A monorepo can be a good fit when:

  • Small team or solo developer — one repository is easier to manage than several.
  • Projects share contracts or code — changes can be made and tested together.
  • Cross-project changes are common — one PR can update multiple applications or services.
  • The whole system is closely related — one repository makes it easier to understand and work across the product.

Polyrepo can be a better fit when:

  • Large or independent teams need clear ownership boundaries.
  • Strong repository isolation is important.
  • Projects have different release cycles.
  • Access control needs to differ between projects.

For Second-Memory, being a sole developer was one of the main reasons I chose a monorepo.

Managing several repositories would add overhead without giving me much benefit. I didn't need repository-level ownership boundaries between teams.

Instead, I wanted to make it easy to work across the web app, mobile app, backend services, and shared contracts.

What Should Be Shared?

A monorepo makes sharing easy.

But easy to share doesn't mean everything should be shared.

I wanted to keep the shared surface area small.

Shared Part Used By Why Share It?
API types Web, Mobile, Backend Keep API contracts consistent
Web-specific code Web Keep it inside apps/web
Mobile-specific code Mobile Keep it inside apps/mobile
Service domain logic Individual service Keep ownership within the service
Service implementation Individual service Preserve service boundaries

So instead of creating shared packages for everything, I only create a package when there is a genuine reason to share something.

For example:

apps/
  web/
  mobile/
services/
  memory-service/
  ask-service/
  embedding-worker/
packages/
  api-types/
  ui-components/
Enter fullscreen mode Exit fullscreen mode

The API types are shared because they represent a contract between different parts of the system.

But a Web-only component stays in apps/web.

A Mobile-only utility stays in apps/mobile.

And Memory Service domain logic stays inside memory-service.

Share the contract, not the ownership

The fact that services live in the same repository doesn't mean they should share their internal implementation.

The API contract can be shared:

flowchart TB
    T[packages/api-types]

    W[Web] --> T
    M[Mobile] --> T
    B[Backend Services] --> T

while the services remain independently owned:

flowchart TB
    MS[Memory Service]
    AS[Ask Service]

    MS --> MD[Memory Domain]
    AS --> AD[AI Interaction]

    MD --> DB1[(Memory DB)]
    AD --> DB2[(Ask Data)]

Share contracts where it makes sense, but don't share service ownership.

Keeping the Shared Surface Small

There is another practical reason for limiting shared code.

The more projects depend on a shared package, the larger the potential impact of a change.

For example:

flowchart TB
    S[packages/shared]
    S --> W[Web]
    S --> M[Mobile]
    S --> ME[Memory]
    S --> A[Ask]

A change to shared may require all projects to be tested.

But a change here:

flowchart LR
    C[Change in apps/web] --> W[Web]
    W --> CI[Web CI/CD]

only affects Web.

This has implications for both CI/CD and application dependencies.

I don't want a large shared package that contains unrelated functionality used by almost every project.

Instead, I prefer small, focused shared packages with clear reasons for existing.

The CI/CD Trade-off

CI/CD is one area where polyrepo can be simpler.

With polyrepo, a change to memory-service naturally belongs to the memory-service repository.

flowchart TB
    R[memory-service repo]
    R --> CI[CI/CD]
    CI --> T[Test]
    CI --> B[Build]
    CI --> D[Deploy]

In a monorepo, multiple deployable units live in the same repository.

A change to:

services/memory-service/
Enter fullscreen mode Exit fullscreen mode

shouldn't necessarily rebuild and deploy everything.

The pipeline needs to understand:

  • what changed
  • what depends on it
  • what needs to be tested
  • what needs to be built
  • what needs to be deployed

This becomes particularly important when shared packages are involved.

A change to ui-components may affect only web and mobile projects, while a change isolated to services/embedding-worker/ should ideally affect only the relevant pipeline.

The repository structure itself is simple, but the build and deployment system needs to become more intelligent as the project grows.

Why Monorepo for Second-Memory?

The main benefits for me were:

  • Easy sharing of API types
  • Easy cross-project changes
  • Easy local development
  • Easy dependency management
  • Easy visibility of the whole product
  • Less overhead for a solo developer

The trade-offs were:

  • More complex CI/CD
  • More tooling needed as the repository grows
  • Less repository-level isolation
  • The need to carefully control shared dependencies

For Second-Memory, I was willing to accept these trade-offs.

The goal wasn't to share as much code as possible.

The goal was to get the convenience of a monorepo while keeping ownership and dependencies clear.

That led to the next question:

How do I manage builds, dependencies, task execution, and caching across all these projects?

Top comments (0)