Loading...
Loading...
March 25, 2026
In the lifecycle of any growing product, there comes a point where the “speed-to-market” hacks of the early days begin to collect interest as technical debt. For my recent project, that moment arrived when our separate web and documentation applications began to drift apart. We had duplicated UI components, inconsistent utility logic, and a build process that felt more like archaeology than engineering.
Maintaining multiple independent applications in a polyrepo-style structure was creating a massive synchronization tax. If I updated a branding token or a core navigation hook in the “web” app, I had to manually port those changes to the “docs” app. This wasn’t just inefficient; it was a breeding ground for bugs.
Dependency management was equally fragile. We relied on relative path imports that spanned across directory boundaries, and without strict enforcement, our internal packages were becoming “leaky.” Developers (and AI assistants) were importing implementation details rather than consuming stable APIs, leading to a “Ball of Mud” architecture where everything was accidentally coupled to everything else.
I decided to refactor the entire ecosystem into a high-performance Turborepo monorepo. The goal was to establish a single source of truth for our UI and logic while enforcing strict boundaries between the apps and shared packages.
I moved our project to pnpm workspaces and implemented a three-tier package strategy:
To solve the versioning headaches, I mandated the workspace:* protocol for all internal references. This ensured that our applications always point to the local, live versions of our libraries, enabling instant Hot Module Replacement (HMR) and ensuring 1:1 parity across the entire codebase.
A senior-level monorepo isn’t just about moving files; it’s about the Directed Acyclic Graph (DAG) of its dependencies. I used our monorepo.mdc project rules to forbid circular dependencies and ensure that logic only flows one way: from the shared core upward to the specific applications.
By combining this with Subpath Exports in our shared package.json files, I created a “black box” around our library internals. Consumers can only import from vetted entry points like @repo/ui/atoms. This encapsulation allowed me to reorganize our internal src/ folders without a single breaking change in the consuming apps.
The results of this migration moved us from “fragmented” to “deterministic”:
For me, this migration was about more than just cleaning up code; it was about building a foundation that scales with the team’s ambition, not just their module count.