The debate on mono vs micro repos rages wild amongst developer circles, and whilst it’s not quite as divisive as tabs vs spaces, I’ve spent some time on both sides of the aisle.
At my last job – which was a FinTech consultancy – we used micro repos extensively. It was incredibly beneficial on one project, and yet on another we found it caused more pain than benefit. So, when I joined HiP, I decided that we’d build out using a mono repo initially, and switch when it sucked. So far, it hasn’t.
However, we do have a handful of different repos — our platform, including all the libraries, microservices, and client / app code live in a single monorepo. However, some of our tools that are part of our build chain – like Tino and Jailbreak live in their own repos. This feels like a pragmatic balance.
There are strong arguments on both sides, and I can see the merit for each approach.
Micro-repos, (where you have many independent repos – typically one per artifact) fit well with the microservices mindset. We ended up using myrepos as a key part of our toolchain, as it made checkout and IDE experiences really pleasant.
Consider using micro-repos if:
- Services (or libraries) evolve independently of one another
- The code is worked on by seperate teams, and release on their own cycle
- You’re treating each service as it’s own product
- Short build cycles, lead to rapid feedback
- When properly encapsulated, repostiories / projects “felt” more lightweight
- Encourages more frequent release cycles. This itself is good, but we found that because we were releasing frequently, we adopted other good practices to make the release cycle smoother – such as to make it smoother, we adopted other good practices, such as Semantic Changelogs and Semantic Release
- Similarly, it enforced good hygiene around versioning and releasing the API of a service / project seperately from the module itself. This friction actually meant we spent more time considering if changes were breaking before introducing them, which lead to more considerate design choices on occasion.
- Allows releases (and upgrades) at different pace. It’s OK across a large estate of services if appA is using v2.3 of LibraryA, but appB is using v2.5. This type of independent evolution becomes more natural using micro repos.
- It’s easy to sprial out-of-control, and find that your spend signficant time managing cross-module dependencies and release cycles.
- It’s difficult to keep project dependencies up-to-date after you release. Releasing libraryA often meant going and updating serviceB and frameworkC – sometimes which also required further releases. Before long, we were in long chains of release cycles.
- When you do have cross-repo changes (which is more frequent at the incepction of services or major features), then PR’s get seperated, and it’s difficult to view the entire set of changes in one place. This leads to reviewers having to hold more knowledge in memory, and we found that it lowered the overall cohesiveness of PR feedback
Microrepos might not be for you (or you might not have your seperation boundaries correct), if:
- You notice that changes frequently span across multiple repositories
- Dependency management gets hard
Note – even when our micro-repos were working well, we grouped repos by feature, rather than by abstraction. This meant that our web code for featureX lived in the same repo as the service code. This was particularly useful, and we found it encouraged more full-stack development across the team.
Monorepos are where all the code lives within a single, large repository. They’re famously used by Google and Facebook – though both have ended up having to build signficant customary tooling to support these approaches.
Consider using mono-repos if:
- You’re a small team, or a team working on a single project / platform that releases together
- You’re new in a project, and don’t yet know where the logical boundaries are. This is the same argument for Monolith Firstservice design.
- Your whole platform evolves and is tested together. Even if you release services independently, if doing so encouarages the rest of the services to upgrade in lockstep – this works particularly well in a monorepo.
- Overall, I’ve found we spend much less time on build admin, despite builds themselves taking longer
- Related to the above, but our module structures are simpler – no more BOM projects, or wrangling the parent pom.
- Builds take longer, and keep increasing as the project grows.
- Tooling for incremental compilation across CD pipelines isn’t where it should be – especially if you’re building containers as part of your build pipeline. (Gradle is good here, but stumbles at the final docker build hurdle)
- It requires much more bespoke build logic for your pipelines to support ‘just release what’s changed’ builds. We’ve deferred this work at the moment, so each release ends up deploying the whole fleet, rather than the new services.
- It’s generally easier for cross-module dependencies to sneak in, so you have to be extra dilligent with dependency hygiene.
- We’re less motivated to actually release, or version artifacts – as a result everything in HiP right now is
0.1.0-SNAPSHOT. In turn, we don’t see the payoff of semantic commit logs described above, which means our commit messages have tended towards lower quality.
- It can become confusing if services don’t all upgrade together. Nothing prevents different modules from depending on older versions of libraries, but it’s confusing if when I’m browsing the code, I go to explore a class, only to find the code that’s being used isn’t what’s in the repo, but some older version pulled down from Maven.
- If you find that changes to certain folders within your repo’s mean that you need special approval (ie., you have a project owner that needs to sign off on changes), then consider break things apart to enable full repostiory developer autonomy.
In general, I’ve found the switch (or return to) a monorepo has been largely positive. However, we need to invest more time in making our build pipeline’s smarter, which is a different type of complexity than we had to manage with micro-repos.
I found that the time micro-repos worked best, was when we started with a mono-repo, and then split when we started feeling pain of build cycles. However, the split out was painful, and took a considerable amount of time. I think if we’d used a tool like Jailbreak to split the repos, it would’ve been much quicker.
Some of the pros and cons listed above are merely circumstantial, and not neccessarily exclusive to mono vs micro repos. For example – being thoughtful around breaking API changes is just as achievable on a monorepo as it is a microrepo. However, we found the more granular release cycle naturally nudged us towards reviewing and releasing these things independently.
Tooling to support microservices in mono-repos is still evolving, and I suspect we’ll see better solutions arrive soon-ish, but for now we’re wearing the cost of slower builds, as a tradeoff for overall higher velocity of features.