DEV Community

CodeNameGrant
CodeNameGrant Subscriber

Posted on

Monorepos: A Year in Review

One Repository to Rule Them All

Its been a year since we started migrating our suite of web applications into a monorepo workspace and I thought it was time share some wins and warts and how we made it all happen.

The goal? To simplify dependency management, reduce code duplication, and make expanding the suite to include more apps or feature libraries easier.

Our team is not the smallest team, but it is the next smallest. With only a two-person development team, every decision mattered—from the scope of the migration project to the order that apps would be moved and how to structure shared libraries. However a small team comes with it the advantage of being able to make rapid decisions without lengthy debates, maintain a high visibility of changes, and experiment with implementation options without affecting a large group of stakeholders.

So lets Jump in, I'm excited to share the results!


Our Approach

A major requirement during the migration was that we couldn’t pause new feature development or bug fixes on existing apps. So we chose to follow an incremental approach: migrating one full app at a time (and all its parts). We started with a smaller app, so we could also focus on setting up the workspace, pipelines, and tooling before scaling to the other applications.

To keep things manageable, we included a caveat, "the app being migrated wouldn’t have new feature development scheduled". Minor patches and bug fixes were fine, but juggling ongoing updates while also migrating those updates risked doubling work and introducing confusion.

The Tech Stack: Nx & Nx Cloud

Of all the monorepo tools, we decided Nx and Nx Cloud were the best fit for us. Its CLI is well-documented, with plenty of examples and tutorials, which made it feel less daunting to learn. This was important—if we couldn’t figure out how to use it effectively, not only would our migration stall, but stakeholders might start doubting whether moving to a monorepo was even a good idea. Nx also comes with plugins for our existing tech stack and Nx Cloud let us experiment extensively before starting the migration with a generous free tier.

Stories from the Journey

Along the way, a few memorable challenges and discoveries taught us more than any checklist ever could.

The Dayjs vs. Kendo Formatting Headache
We ran into a sneaky bug when our dayjs formatter clashed with KendoReact. Day.js uses ISO formatting (eg. YYYY for a 4-digit year), while KendoReact expects its own notation (ie. yyyy). The fix was to use Day.js to handle all calculations and comparisons, and leave all the formatting to KendoReact, keeping everything consistent.

Rolling Our Own Keycloak Wrapper
The React Keycloak wrapper we originally used was outdated and barely maintained. With no other alternatives, we built our own, learning token flows, refresh cycles, and how React talks to Keycloak. The result: a reliable, battle-tested wrapper and a team that actually understands how authentication works.

The Refactor That Ate the Repo
Refactoring a set of shared UI components started ith teh best of intentions, and suddenly the refactor had touched 150+ files. Nope! The PR was far too big to review safely, so we had to reverted it. Instead, we tackled updates piecemeal with smaller, focused PRs—easier reviews, less risk, and more confidence. Lesson learned: big-bang changes feel efficient, but controlled iteration wins.

The Documentation We Didn’t Know We Needed
We’d never focused much on documentation—who does, knowledge lived in our heads mostly. But when we invited an external team to review our Nx workspace, we had to actually write things down. At first, it felt like busywork, but it clarified our reasoning, exposed gaps, and suddenly onboarding a new dev didn’t seem impossible. What started as “just for the reviewer” ended up being for us: documentation became a tool for thinking.


Code That Could Stay Put

Not everything needs to be migrated. A useful lens is to ask: Is it valuable, used often, and under our control? If not, it might be better left alone.

  • Short-lived or legacy code — End-of-life systems, POCs, and prototypes are rarely worth the effort.
  • Low-impact tools — Admin dashboards, internal scripts, or utilities used by a handful of people could be evaluated case-by-case and categorised as low-priority to be done after the main goals have been achieved.
  • Stable or performance-critical modules — For code that’s rock-solid or finely tuned for performance, don’t prioritize moving it just for the sake of uniformity. Wait until change is truly needed.
  • Archived or compliance projects — Kept only for historical or legal reasons, these don’t need to me moved.

Side Effects We Actually Liked

On top of the main goals, there were a few extra benefits made the year even sweeter…

  • Zero version drama – Honestly, we hadn't thought about keeping the workspace up to date during the migration, but even before all apps were fully migrated, we were running workspace-wide updates between app migrations. By using nx migrate, Nx auto-updated all its dependencies for us. Genuis!
  • Smoother developer experience – With shared libraries, single source of truth, and consolidated utilities, knowledge sharing became easier. Tracking components or utilities was simpler, and both local development and onboarding became more predictable.
  • Reviews that matter – Shared modules are tested rigorously, so we no longer need to review every utility implementation. Code reviews focus on feature logic instead of rehashing the same patterns, a big improvement over the old polyrepo setup.
  • Team Trust in Action - Full disclosure: at first we didn't have default branch protection policies in place, not because we wanted to avoid code-reviews, we just couldn't get the GitHub rules sets to behave the way we wanted. We still practiced code reviews, they just weren't enforced. It’s not perfect, but it worked for our small, trusting team—and it shows there’s room to grow.

Lessons Learned

A year into our monorepo migration, a few key lessons stand out—lessons that helped us keep the project moving while learning along the way.

1. Start Smart
Before touching production, we spun up many test workspaces and pipelines. Experimenting early gave us confidence and reduced “gotchas.” At the same time, establishing clear boundaries—deciding which apps or modules to migrate and what fell in our mandate—kept the work manageable. Nx generators like @nx/workspace:move made it easy to adjust structure later, so we didn’t need to get everything perfect from day one.

2. Move in Steps
Migrating one app at a time allowed us to validate our setup and refine tooling before tackling bigger projects. Each app became a learning opportunity rather than a risk. We also learned to accept that first-pass solutions wouldn’t cover every edge case. Some duplication or temporary shortcuts were inevitable, but planning to refine incrementally kept momentum going without compromising quality.

3. Keep It Practical
It’s tempting to overengineer: abstract everything, customize every build, and aim for perfection. In reality, sticking to practical choices—leaning on defaults, balancing abstraction, and making deliberate trade-offs—kept progress moving. Sometimes duplicating a component temporarily was faster, with the intent to refactor later once all use cases were understood. Pragmatic decisions lead to reusable code without slowing development.

4. Document as You Go
Writing down assumptions, documenting shared components, utilities, and conventions clarified our thinking and improved onboarding. Initially, it felt like busywork for an external reviewer, but it quickly became an internal tool for tracking decisions, spotting gaps, and making the migration more maintainable. Good documentation isn’t just for others—it reinforces clarity for your own team.

5. Clean as You Go
Unexpected quirks, outdated dependencies, and long-forgotten scripts surfaced repeatedly. Each one became an opportunity to clean up and standardize. Tackling these early prevented bigger problems down the line and improved overall code quality.

6. Build Confidence
Rigorous testing in shared modules turned out to be a huge time-saver. With solid test coverage, we no longer needed to review every utility implementation, and refactoring or updating apps felt safer. Testing wasn’t just about catching bugs—it reinforced trust in the shared code and the migration as a whole.

Taken together, these lessons show that a thoughtful, incremental approach—prioritizing practicality, documentation, and testing—makes a monorepo migration manageable, educational, and ultimately rewarding.


Conclusion

One year in, the monorepo has delivered more than just technical improvements—it has transformed how we work. Getting here wasn’t simple. Each app migration involved careful planning, rethinking abstractions, and multiple iterations as new requirements and edge cases emerged. There were moments of trial and error, but every challenge taught us something valuable.

Today, apps are consistent, upgrades are simpler, and bootstrapping new applications is faster thanks to shared layouts, utilities, and workflows. CI/CD automation still has room to grow, onboarding could be smoother, and shared libraries will keep evolving—but the foundation is solid, and day-to-day development is now more predictable and less error-prone.

For small teams, the lessons are clear: embrace an incremental migration, prioritize practical reuse over perfection, and experiment and leverage tools like Nx to make things easier for you. The monorepo isn’t just a technical shift—it’s a cultural one, and the effort has been well worth it.

One repo, many lessons, endless possibilities.

Top comments (0)