← Back

From Multi-Repos to Monorepo: A Practical Migration Story

Every new feature required four pull requests. One for the API, one for the admin panel, one for the web app, and sometimes one more for mobile. Each PR lived in a different repository. Each repository had its own CI/CD, its own structure, its own deployment pipeline. Coordinating all of that was slowing us down.

This is the story of why we moved from multi-repos to a monorepo — and what we learned along the way.

Starting with Multi-Repos

When we started building our travel reservation app, we were a small team. Most members were side job contractors. The teams were divided into four groups: iOS, Android, Backend (API + Admin Panel), and Web (App Webview).

Each team moved at a different speed. Not all of them started development at the same time. A multi-repo strategy made sense at that stage. Each team could decide their own structure, pick their own libraries, and set up their own CI/CD without impacting each other.

For an MVP with a small team, this worked well. The independence was the point. No merge conflicts between teams. No waiting for another team’s pipeline. Everyone moved fast in their own space.

Where Multi-Repos Broke Down

As the team grew, the problems became hard to ignore.

Development speed dropped. A single API change meant creating PRs in multiple repositories — the API repo plus every affected client repo (Admin, Web, iOS, Android). What should be one change became three or four coordinated changes.

Team coordination got harder. Understanding the context or impact of one change was difficult. Each team moved at their own speed, and code visibility between projects was poor. Refactoring an API meant tracking changes across repositories with no shared view.

Deployments required choreography. Each team generated PRs on different repositories. We had to coordinate the merge order and deployment sequence to avoid downtimes. Backward compatibility was not optional — it was a constant overhead.

Testing environments were manual work. Creating a testing environment for a specific feature meant identifying every related PR across repos and deploying them together. There was no single branch that represented the full feature.

The pattern was clear. The cost of coordination was growing faster than the team. The multi-repo strategy that gave us independence in the beginning was now creating friction at every step.

Moving to a Monorepo

After identifying these problems, we started looking for ways to increase development speed. A monorepo was the natural answer.

We took a hybrid approach. The Web, API, and Admin repositories moved into a single monorepo. The iOS and Android repositories stayed in their own repos. This was a pragmatic decision — the TypeScript projects shared the most code and had the most coordination problems.

What Improved

One source of truth. Instead of jumping between repositories, all TypeScript projects lived in one place. Code visibility improved immediately. Every team could see what every other team was doing.

Sharing code became simple. Common utilities, types, and configurations could be shared across projects without publishing packages or managing versions. One change, one place.

Refactoring across projects stopped being scary. Changing an API and updating every consumer happened in a single PR. No breaking changes across repositories. No coordinated merges.

Deployments became straightforward. All the code for a feature lived in one branch. Deploy that branch, and everything goes together. Testing environments could be created from a single branch with the full context.

What Got Harder

A monorepo is not free. It introduces its own problems — but most of them have well-known solutions.

Package linking gets complex. Managing dependencies between packages inside a monorepo requires tooling. We solved this with Turborepo, a build system designed for TypeScript monorepos. It handles caching, task orchestration, and dependency resolution.

Access control disappears. A single repository means all members see all code. For us, this was actually a benefit — we wanted better code visibility. But if a project needs strict access control, a separate repository is the right call.

Commits can get messy. Changes across multiple projects in one commit make history hard to read. We solved this with a commit linter using conventional commit conventions. Every commit follows the same format:

<type>(<scope>): <description>

refactor(newt-ui): remove unused Button component
fix(newt-ui): include onClick prop Button component
test(newt-ui): add test for Button component
chore(global): ignore some folder in .gitignore

Tooling That Made It Work

The monorepo would not work without the right tooling. Turborepo handled builds, caching, and task dependencies. Husky ran linters and tests before every push through git hooks. Vercel gave us preview environments for every branch — frontend and API together. GitHub Actions ran CI/CD per project using path filters:

on:
  push:
    paths:
      - "frontend/storybook/**"

Each tool solved a specific friction point. Without them, the monorepo would have created more problems than it solved.

The Real Lesson

The decision between multi-repo and monorepo is not about which is better in absolute terms. It depends on the team, the stage, and the coordination cost.

Multi-repos gave us independence when we needed it. The monorepo gave us speed when coordination became the bottleneck. The best practice I have found is to start with whatever reduces friction for the current team size — and be willing to change when the tradeoffs shift.

The important thing is recognizing when the current strategy is costing more than it gives. For us, that moment was when every feature needed four PRs and a deployment plan. The monorepo did not solve every problem. But it removed the biggest one: the cost of working together across projects.