# 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:

```text
apps/
  web/
  mobile/

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

packages/
  api-types/
  ui-components/
```

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:

```text
apps/
  web/
  mobile/
services/
  memory-service/
  ask-service/
  embedding-worker/
packages/
  api-types/
  ui-components/
```

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:

[![](https://mermaid.ink/img/pako:eNpVjs1ugzAQhF8F7RmowfzFhxxorzkVKVJxDg7eAArYyECbFOXda4JaqXva-UYzuwtUWiIwuHT6q2qEmZwi58qxU5SDqK6ixvFFDK033QccT1xt5rE84vnkeN7eKTZyKA_63Hb4D-ZlbjtQSecdzWdb2YbNBhdq00pgk5nRhR5NL1YJyxrkMDXYIwdmV4NyvnlSmKtX6U4bDlw9bH4Q6kPr_rfC6LlugF1EN1o1D1JM-NaK2oj-jxr7CZpXPasJWBg-O4AtcLMqon4WBWGYZCTdBXGcuXBfMfFpFJE4DSihNEnShwvfz7PET4jlNKa7lNIss9YPMCdlRw?type=png align="center")](https://mermaid.live/edit#pako:eNo9jU9rhDAUxL9KeGeVaPyTzWHBtpeChdJj1z0Ek1WpSeQ1odvKfvfGhfad3vyGmdlgcEqDgMvivoZJoifdW29JvPbUrusyD9LPzp5Jmh7Jw6nrXkj7-nyGBEacFQiPQSdgNBq5S9j2cA9-0kb3IOKLWoVrqiR-pINbHPbQ21vMr9K-O2f-KtCFcQJxkctnVGFV0uunWY4ozT9FbZXGRxesB5HzeweIDa4gipJlvMyLoua0OeRVFd3vHdOMlSWtmpxRxuq6uSXwc5-lWU0jZxU7NIxxHq1fsThRoA)

while the services remain independently owned:

[![](https://mermaid.ink/img/pako:eNpNjU9vgkAUxL8KeSdNwADLPzk0ge7FAyd6KnjYwFOJsGueS6s1fveuNGj3tDPzfjM3aFSLkMKuV9_NQZC2PvJaWuYVZVXgoOhqlUhfXYPbPz8rq-x8_GfO55bjvFkFnymuBtHJJzSlGa-yjbWRGkk0ulPyhfPpgOdetZgL8uVMz6FfLR7bXGix3IINe-paSDWNaMOAZAaNhNuDqkEfcMAaUvMlbMeL0wo6Oo3qFdVQy7vhT0J-KjXMFaTG_QHSnejPRo2nVmjkndiTGJ4uoWyR3tUoNaR-NHVAeoOLUQFbJYHn-1HixmsvDBMbrg_bXbEgcMPYYy5jURTfbfiZZt1V5BqfhWwdM5YkJvoFodR4QQ?type=png align="center")](https://mermaid.live/edit#pako:eNpVjs1ugzAQhF8F7RmowfzFhxxorzkVKVJxDg7eAArYyECbFOXda4JaqXva-UYzuwtUWiIwuHT6q2qEmZwi58qxU5SDqK6ixvFFDK033QccT1xt5rE84vnkeN7eKTZyKA_63Hb4D-ZlbjtQSecdzWdb2YbNBhdq00pgk5nRhR5NL1YJyxrkMDXYIwdmV4NyvnlSmKtX6U4bDlw9bH4Q6kPr_rfC6LlugF1EN1o1D1JM-NaK2oj-jxr7CZpXPasJWBg-O4AtcLMqon4WBWGYZCTdBXGcuXBfMfFpFJE4DSihNEnShwvfz7PET4jlNKa7lNIss9YPMCdlRw)

> **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:

[![](https://mermaid.ink/img/pako:eNpVjUtvgzAQhP8K2jNQg3nFh0jp48iplSIVcnDwBhDYRgbUpCj_vSZqK7GnnW92ZxaotEBgcOn1V9VwMzkfz6Vy7LwXA686XuP4NFoDxemXO563d47FEc8bkhe5Prc9buFbkaPU5rahh-IwdidwoTatADaZGV2QaCRfJSzrbQlTgxJLYHa17fPVE9x0XqV7bUoo1d3-D1x9ai3_Ioye6wbYhfejVfMg-ISvLa8Nl__UoBJoXvSsJmA0fmQAW-AKLIyon0VBGCYZSXdBHGcu3FZMfBpFJE4DSihNkvTuwvejlvgJsZzGdJdSmmXW-gHi2GSi?type=png align="center")](https://mermaid.live/edit#pako:eNpNjU9vgkAQxb8KmZMmYIDln3toAt2LB0_2VPCwgVGJsGvGpdUav3tXGrRzmnlvfu_doNYNAoddp7_rgyTjfBSVcuysN-Uae01XZ4P01da4_dPzTZmfj__E6d3xvDdnLSZK6F626gmNbi7KfOWslEGStWm1euFifBBFUM6mgGI-0ZMZlrNHt5BGzrfgwp7aBrihAV3okWyhPeH2oCowB-yxAm5Xwma4eI2ko1frTlMFlbpb_iTVp9b9FEF62B-A72R3ttdwaqRB0co9yf6pEqoG6V0PygBnwZgB_AYX4GHEFlkUhGGS-ekyiOPMhetD9hcsivw4DZjPWJKkdxd-xlp_kfhWZzFbpoxlmbV-AaCNeD0)

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

But a change here:

[![](https://mermaid.ink/img/pako:eNo9jU9vgzAMxb9K5DPQQPibQy_0MmmnXZBWesiIC2iQIBfUbqjffYFp88Xv_Ww_r9BYjSDhOth70yma2etbbZir8lx2yrTIesPUNN0Od_y4MN8_supcOfm7Ve2kfNmQa4fydAEPWuo1yJkW9GBEGtVmYd1Oapg7HLEG6SShXh6-VvTpN3awVENtnu5-Uubd2vEvguzSdiCvarg5t0xazXjqVUtq_KeERiOVdjEzSFHsGSBXeICMYhHkcRhFac6zIkyS3IOvDfNAxDFPslBwIdI0e3rwvb_lQcodF4koMiHy3I1-AHC-WUA?type=png align="center")](https://mermaid.live/edit#pako:eNpVjUtvgzAQhP8K2jNQg3nFh0jp48iplSIVcnDwBhDYRgbUpCj_vSZqK7GnnW92ZxaotEBgcOn1V9VwMzkfz6Vy7LwXA686XuP4NFoDxemXO563d47FEc8bkhe5Prc9buFbkaPU5rahh-IwdidwoTatADaZGV2QaCRfJSzrbQlTgxJLYHa17fPVE9x0XqV7bUoo1d3-D1x9ai3_Ioye6wbYhfejVfMg-ISvLa8Nl__UoBJoXvSsJmA0fmQAW-AKLIyon0VBGCYZSXdBHGcu3FZMfBpFJE4DSihNkvTuwvejlvgJsZzGdJdSmmXW-gHi2GSi)

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.

[![](https://mermaid.ink/img/pako:eNpFTUFugzAQ_Iq1ZyAGYyA-5EIulXLKBalxDi7eACrYyAElLcrfa6iq7mVnZmdmF6itRhBw6-2jbpWbyOksDfFTXspWmQZJZ4gax_vugR9XEoYHUl0qD39d1aaUb6vk1648XiGAxnUaxORmDGBAN6iVwrJGJEwtDihBeOhQz89QK_cZ1ra3ToI0L58flXm3dvircHZuWhA31d89m0etJjx2qnHq34JGoyvtbCYQnG4VIBZ4gkhSFhVpnCRZQfN9zHkRwNcq04ilKeV5zChjWZa_AvjevtIoo15nnO1zxorCn34AEW9Y7g?type=png align="center")](https://mermaid.live/edit#pako:eNo9jU9vgzAMxb9K5DPQQPibQy_0MmmnXZBWesiIC2iQIBfUbqjffYFp88Xv_Ww_r9BYjSDhOth70yma2etbbZir8lx2yrTIesPUNN0Od_y4MN8_supcOfm7Ve2kfNmQa4fydAEPWuo1yJkW9GBEGtVmYd1Oapg7HLEG6SShXh6-VvTpN3awVENtnu5-Uubd2vEvguzSdiCvarg5t0xazXjqVUtq_KeERiOVdjEzSFHsGSBXeICMYhHkcRhFac6zIkyS3IOvDfNAxDFPslBwIdI0e3rwvb_lQcodF4koMiHy3I1-AHC-WUA)

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

A change to:

```text
services/memory-service/
```

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?**
