Monorepo in Practice (part 1): Between “Please Don’t” and “Please Do”
Denis Bratchikov

Denis Bratchikov @denis_bratchikov

About: 👨‍💻 Full-stack Developer passionate about architecture and AI-driven Solutions. Sharing best practices, case studies, and tools from daily experience.

Location:
Berlin, Germany
Joined:
Feb 25, 2025

Monorepo in Practice (part 1): Between “Please Don’t” and “Please Do”

Publish Date: Jul 15
0 0

This post is part 1 of 2. In this one:

  • Why we ditched submodules and went monorepo
  • How our structure looks now
  • What to consider before migrating (team buy-in, effort, risk)


Introduction

Not long ago, I stumbled upon two classic articles: Monorepos: please don’t! and Monorepo: please do!. It's been six years since those posts were written, but the topic is still very much alive.

That got me thinking: how much has really changed since then? In this post, I’ll share our journey moving to a monorepo for our frontend, what worked, what didn’t, and what surprised us along the way.

For context: Our codebase is ~8,700 files, 700k lines of code, and around 1.2GB (including test screenshots). We’re a team of 25–30 frontend devs working full time on it.


1. WHY?

Why on earth did we decide to switch to a monorepo?

To answer that, let’s take a look at the setup we had right before the migration:

  • A few shared packages (like ui, configs) in separate repos, published to GitHub registry
  • app X — React (CRA 🫠) app, deployed daily on Vercel with Playwright integration tests
  • app Y — Next.js app (dashboard + landing pages), also deployed daily on Vercel, but uses the main application via GitHub submodules (yep, legacy from when we had only 2 apps)
  • Each app had its own GitHub CI pipeline
  • The engineering team was growing — we no longer fit in one room, and management started dividing us into smaller teams with distinct business areas... but the code was still shared

This setup caused us a lot of problems.


First — GitHub submodules.

Surprisingly, not many people have worked with them before, so ~70–80% of new hires struggled with them. But that wasn’t even the worst part.

Imagine you’re mostly working on app X. You update a component, tweak its behavior and contract, update all its usages, run tests — CI passes — great! You merge it.
All good. Everything works.
Then, someone updates the submodule SHA in app Y — just one line changed — and boom, CI breaks. Why? Because you forgot to update app Y when you changed the contract.

Congrats, your 5-minute fix just turned into a 1-day headache.


Second — delivering fixes to core packages.

Let’s say you fix a bug in the UI components. You’re fast — 15 minutes and it works perfectly in Storybook. But now comes the “real” flow:

create PR → wait for review → wait for QA → merge and publish the package →
create PR to update app X → review → QA → merge →
create PR to update app Y → review → QA → merge →
scratch your eyes out and consider a career in goat herding.

Now imagine doing that at scale... or with late feedback.


Third — ownership and code boundaries.

We were already moving toward smaller, purpose-driven packages with clear ownership. This could work in both mono- and polyrepo setups. We didn’t need true microservices (felt like overengineering), but we did need structure.

And on top of the submodule mess, polyrepo gave us even more headaches:

  • Every repo needed its own GitHub config: roles, branch rules, required CI jobs...
  • Developer experience was rough: > run package A → symlink it to B → symlink B to C → symlink C to app Xfinally start debugging
  • Knowledge sharing was a joke — unless you proactively hunted through GitHub, you’d have no idea new packages even existed

Having all this in mind, we decided to migrate to a monorepo (Turborepo) setup.


2. What's the current status?

It’s been almost a year since we migrated to a monorepo. After polishing things up and iterating a bit, we’ve landed on the following setup.

2.1 Folder structure

  • apps/* — all applications (both production and dev)
  • packages/features/* — functional, domain-specific feature packages
  • packages/* — common/shared utilities and modules
  • configs/* — all configuration code (ESLint, TS configs, custom plugins, etc.)
  • scripts/* — various dev scripts and automation tools

Our Files Structure

2.2 Tech stack

2.3 Compilation strategy

Turborepo supports three main compilation strategies:

  • Just-in-Time Packages
  • Compiled Packages
  • Publishable Packages (not relevant for us — we don't publish internal packages)

We initially went with compiled packages — mostly because it was the easiest to implement given our (legacy) tech stack and tight timeline. It worked, kind of. But now we’re investing time into switching to just-in-time strategy instead. Why?

  • It requires far less configuration (and less risk of misconfiguring things).
  • You don’t need to rebuild packages every time you build the app.
  • It enables better code sharing. For instance, we use postcss custom media defined in the ui package - but it doesn’t get propagated to other packages because postcss is stripped out during compilation.

So yeah — compiled got us up and running fast. But just-in-time is what’s going to scale.


3. Our Migration Story: Time, People, and Pain

- "Where’s the money, Devowski?"
- "It's uh... it's in monorepo somewhere, let me take another look."

So - how long does it take to migrate? And what should you think about before even starting?

Well, first: define your scope - how many apps and packages you’re migrating, your time budget, and what you’re hoping to gain. That will shape everything else.

In our case, we had a company initiative called Dev Week — one week where developers aren’t obligated to work on business features and can instead invest in tech debt or internal tools. We figured: perfect timing. Let’s migrate the whole codebase to a monorepo in one shot.

Here’s how we organized it:

  • A 6-person team
  • 2 devs focused on the CRA-based app (legacy-heavy)
  • 2 devs handled the Next.js app (that depends on the CRA one via submodules)
  • 2 devs worked on infrastructure, migration scripts (for open PRs), and CI pipelines
  • We wanted to preserve git history, so we used git filter-repo to merge the old repo histories into their respective folders

By the end of the week, we had... a solution that just about worked.

The CI pipelines were mostly green, preview links were up, and we could ship from the monorepo - so we did.

Of course, we hadn’t caught everything - Sentry config, Storybook setup, some edge cases - so we spent the following week fixing and polishing.


But migrating code is only half the story.

You also need to prepare your team for that shift.

From this point on, your devs aren’t in isolated repos anymore — they’re part of a bigger, interconnected system. There's no more "my code" vs "their code" - now it’s "our code I know" and "our code I don’t know yet".

Moving to monorepo isn’t just a git decision — it’s a team, process, and trust decision.
Make sure you're not only moving code, but moving minds too.


This is the end of part 1. In Part 2, I’ll cover:

  • Where monorepo shines
  • Where it sucks
  • My honest take: when you should and shouldn't do it


Feel free to connect with me on LinkedIn

🚀 More content coming soon - stay tuned!

Comments 0 total

    Add comment