The Monorepo: Why Prepared 911 Embraced a Unified Code Repository
Shifting to a monorepo structure was an unexpected yet transformative decision for Prepared. Traditionally, the very idea of housing our Ruby server alongside our React front-end console seemed not just counterintuitive, but counterproductive.
I'll admit, my initial thoughts leaned towards skepticism: “Is this wise?”; “Won't this complicate our code tracking?”
Contrary to my apprehensions, the monorepo system has paved the way for rapid, robust development. The process of integrating both front-end and back-end changes within a single PR surprisingly minimized potential bugs prior to merging into the main branch.
In this post, I'll detail our experience at Prepared 911, highlighting the benefits and challenges of adopting a monorepo with our specific tech stack and team composition.
Background:
Our Tech Stack:
- Ruby on Rails
- GraphQL API layer
- Front-ends using React TS
- Services in Go
- GRPC Communication between services
Our Original Structure (simplified):
- React Front End Client #1
- React Front End Client #2
- React Front End Client #3
- Electron Desktop App
- Ruby on Rails API
- Go Service
Transition to MonoRepo (simplified):
- App
- ~Ruby:
- ~~API
- ~TS:
- ~~Front End Client #1
- ~~Front End Client #2
- ~~Front End Client #3
- ~~Desktop App
- ~~Shared React Code
- ~Go:
- ~~Service #1
- ~~Service #2
- Dev
- ~Shared Shell
Monorepo: The Pros & Cons
Pros:
Unified Development & Maintenance
- Before: Before the monorepo, working on a feature that involved changes across 4 different projects required creating 4 different pull requests, one in each repository. That required me to run projects locally, create PRs in each repo, and ask the reviewer to check all branches and correlate work across repositories. This made both the reviewing and QAing a task tedious.
- With Monorepo: All changes are localized within a single repository, removing the need for multiple PRs. There's no longer a need for reviewers to toggle between different repositories, making the review process more focused and less prone to missing things. In a startup, where the reviewer is super busy, if it’s difficult to correlate and test things locally, this step would often be skipped. (For counter-argument: see Cons below)
Efficient PR Environments
- Before: We always aspired to use PR environments, but coordinating them across different repositories was a challenge. API changes needed to match the front-end changes, which needed to match each microservice. Unless there is a unique identifier for the CI to match between, this would be very challenging. Thinking through how to set up a PR environment was a challenge and thus the task was never prioritized due to its complexity.
- With Monorepo: Setting up PR environments was incredibly straightforward. We can detect changes in a specific folder, build docker images, and deploy them seamlessly. Any folder that did not have a change, the main branch image would be used. After transitioning, we were able to set up PR environments in just a week. Introducing new services to this PR environment is also hassle-free. Implementing this process has made PR environments an essential tool for our developers during development and reviews.
Seamless Front-End Module Sharing
- Before: Previously, if we wanted to share code, it meant either creating an NPM package and committing to another repo, then checking out the code in the two shared projects and making sure all the types worked out. This was a very difficult process and thus we never (or very rarely) set it up properly.
- With Monorepo: All front-end modules reside under a single <code>/ts/</code> directory, allowing easy sharing of common React libraries. This eliminated the need for version management of shared files and made integrating shared components more intuitive. This has made the shared code repository much easier to implement, and thus more likely to be used by the developers.
Integrated Code Generation (GRPC, GraphQL)
- Before: As we moved towards more backend services using GRPC, managing proto files and ensuring proper communication between services became complicated. Proto files had to be shared across repositories.
- With Monorepo: We can directly reference proto files defined in one service and use them to generate code in the other services. This direct reference without any intermediary steps streamlines the communication setup, making it easier to make API changes.
Standardized Command Line System
- Before: Different repositories had their commands in their specific language (yarn, rails, etc), and developers had to dive into files or readmes to understand them. This became a complicated web that requires a lot of context to navigate.
- With Monorepo: A unified command structure provides a consistent interface to program across the monorepo.
Commands like <code>just run</code> simplify tasks, and the ability to create directory-specific commands (like <code>just api run</code> or <code>just dispatch run</code>) offers easy access to any module from any other module.
This shared command line system makes onboarding new engineers smoother, as it provides a standardized set of tools across all projects. There is also an obvious place to put frequent maintenance commands like <code>just login development</code> which would go into the <code>/dev/</code> route. This command interface is simple and easy to add to, making developers more likely to use and improve them!
Cons:
Bulkier PRs
- Background: With a monorepo setup, PRs can become extensive, often running into thousands of lines of code.
- With Monorepo: While a single comprehensive PR can give a full picture of a feature, it can be overwhelming for reviewers. To mitigate this, we've begun breaking down PRs into sequences built off each other. For instance, PR1 could handle the Backend, PR2 the Front End (with PR1 as its base), and PR3 for UI Cleanup based on PR2. This hierarchical structure helps in maintaining clarity during reviews.
In Retrospect
Switching to a monorepo was undoubtedly one of our best infrastructural decisions. It not only augmented our development efficiency but also opened doors to streamlined operations, consistent PR environments, and an integrated command line system.
My personal productivity has seen a significant boost, enabling me to undertake and complete intricate projects faster. I would advocate startups, especially those similar in size and technical stack to Prepared, to contemplate this strategic shift.
When standards make the development process easier and simpler, developers are more likely to adopt and improve them as they use them more. This is clearly the case for the monorepo and the tooling we have created around it @ Prepared.
As the team grows, we may out grow the monorepo, but for today it is here to stay.
If you're curious about our transition or have any questions, feel free to get in touch: neal@prepared911.com.