Introduction: Why This Story Might Save You Some Headaches
If you’ve worked in a large codebase, you’ve probably seen this: the code runs, but every change takes far longer than it should. Updating one feature often breaks another. Adding a dependency means worrying about conflicts. Old build tools still linger, and no one feels safe upgrading packages that half the system depends on.
That’s exactly what we faced in our journey-monorepo
, the main repository for our team. Dependencies were wired together in inconsistent ways, different teams had introduced multiple overlapping build tools, and some libraries hadn’t been touched in years. Proposing a cleanup felt like asking to redesign a city while people were still living in it — risky, disruptive, and easy to delay indefinitely.
So why did we bother?
- Development had slowed to a crawl. Builds were taking too long, and waiting minutes just to see a change in the browser was killing productivity.
- Every change felt risky. The dependency spaghetti meant a small tweak in one package could break multiple apps — often in ways we wouldn’t notice until much later.
- Onboarding new developers was painful. It took too long for new team members to get up to speed, mostly because they had to learn multiple bundlers, testing setups, and outdated tooling quirks.
- Technical debt was blocking new features. We were spending more time patching, debugging, and wrestling with the repo than building the actual features our users cared about.
- Costs were adding up. Longer build times meant higher CI costs, and outdated packages made security updates and maintenance more expensive in the long run.
In short, the longer we waited, the more time, money, and momentum we were losing.
And, hey, now we get a good story out of it.
Journey-Monorepo in a Nutshell
Journey-monorepo is the central place on the epilot platform for everything related to journeys. What you see in the screenshot above represents two main apps: journey-app on the left side and journey-builder on the right. Journey-builder is responsible for configuring journeys, and journey-app for rendering them.
Like in many startups, the repo started out clean and well-structured, but as the product grew quickly, the codebase expanded in unplanned ways — things were added wherever they seemed to fit at the time.
The “BEFORE” Setup
Apps:
- journey-builder.
- journey-app
- entity-mapping
- journey-view
Packages:
- blocks-configurators
- blocks-renderers
- journey-elements
- journey-utils
- journey-logics-common
Let me show you the rough map of dependencies we had step by step. We are interested in two main apps: journey-builder
and journey-app
, so I am going to dive deeper into their connections. It is not important which app or package does what. What IS important is to see how many connections we had to keep in mind every time we needed to change anything.
You might say: Kate, come on, this is what happens in every big project - things become deeply intertwined, and it is a normal thing to experience.
I will answer: yes, but I haven't finished.
Let's now add the build tools to this table to have the full picture.
Well, what we have here:
- Webpack;
- Craco;
- Tsup;
- Tsx;
- Tsdx.
I don't know about you, but I hadn't worked with half of them before I started working on this repo.
In the end, it was a web of “if you change this, you might break that” — and “that” could be anywhere.
How We Tackled It
We kicked things off with an RFC — basically, “let’s write this down so we don’t lose the plot halfway through.”
Our two big goals:
- Unify the tooling so we weren’t juggling five different build setups.
- Untangle dependencies so teams could work without stepping on each other’s toes.
And this is exactly what we did.
Step 1: Cut the Cord on Unnecessary Dependencies
The first thing we had to do was face the dependency jungle. Over time, different teams had introduced overlapping packages, and no one was quite sure what depended on what anymore. Cleaning this up gave us a chance to regain control.
Untangled all the apps and packages as much as possible. We mapped out how everything connected and then started cutting back the unnecessary links. This meant reducing cross-dependencies that caused fragile builds.
Moved blocks-configurators into journey-builder. These configurators were never meant to be used outside, so pulling them closer to where they belong simplified the dependency graph.
Moved blocks-renderers into journey-app. Renderers were core to the application itself, so consolidating them in the main app made the architecture more intuitive.
Deleted journey-utils and journey-elements. Journey-utils was a very small package with some utilities that were moved to other packages. And journey-elements _ were later replaced by _concorde-elements. You can read more about it in this article written by my partner in crime.
This was a painful step, but once the dust settled, the repo felt lighter and easier to reason about. We no longer had to worry about accidentally breaking other parts of the system when making small changes.
Step 2: Standardize the Tooling
After trimming the dependencies, the next big challenge was our tooling. Different parts of the repo used different build systems, which slowed everyone down and made onboarding new developers a headache.
Phased out Craco, Tsup, and Tsdx by switching to Vite. These older tools had been added over time, each solving a narrow need. Vite gave us a unified, modern setup with faster builds and simpler configuration.
Swapped Jest for Vitest — faster tests, modern setup. Jest had served us well, but it was slow and required custom patching to keep working. Vitest gave us near-instant feedback and worked seamlessly with Vite, making the dev experience far smoother.
By unifying the tooling, we not only sped up day-to-day work but also reduced cognitive load. Developers no longer had to remember which tool applied to which package — everything just worked the same way everywhere.
This is how the "AFTER" setup looks:
As you can see, the monorepo is now far more streamlined — there are significantly fewer dependencies crisscrossing between packages, and we’ve eliminated most of the variations in build tools. This makes the structure easier to understand, faster to work with, and much less fragile when changes are introduced.
After the Restructuring: What We Learned
When the dust settled, it wasn’t just that the dependency graph was cleaner — we were thinking about the repo differently.
Immediate Wins
Changing things stopped feeling dangerous. Before, touching a shared package felt like pulling a brick from a Jenga tower. Now, fewer connections mean less risk.
Builds and tests stopped feeling like a punishment. Vite’s dev server = instant feedback. Vitest shaved minutes off test runs. Devs started running tests more often just because they could.
Onboarding went from “ugh” to “okay.” New folks could get set up without learning the quirks of four bundlers first.
Slower Payoffs
- Tooling swaps aren’t magic. Some Webpack plugins didn’t have a Vite equivalent, so we had to rethink features or drop them.
- Vitest exposed bad tests. Some “passing” tests were only passing thanks to Jest’s quirks. Painful, but worth fixing.
Trade-Offs & Reality Checks
- Not everything got unified. A couple of apps still run on legacy setups because migrating them wasn’t worth the risk — for now.
- Shared packages need rules. We now ask “does this really need to be shared?” before creating new ones.
- Refactoring is never “done.” There is always room for improvement, we all know. that.
Tips If You’re Thinking About Doing This
- Draw your dependency graph before you start — it’ll help convince others (and yourself) that the cleanup’s worth it.
- Expect setbacks. Some PRs will break things. That’s normal.
- Automate checks for unused or outdated packages — saves a ton of review time.
- Celebrate small wins. Removing the last Webpack config deserved cake.
Final Thoughts
This wasn’t a quick tidy-up — it was a half-year-long deliberate overhaul alongside working on other features. But it’s made a massive difference to how we work: faster builds, fewer “don’t touch that” areas, and a general feeling that the repo is ours again, not a beast we’re afraid of.
If I had to wrap it all up in one bit of advice?
Keep it simple.
Every extra dependency, tool, or package you skip today is one less thing you’ll have to untangle in the future.
Top comments (0)