Monorepo is a common thing among the biggies in the IT world, like Netflix or Google. In our situation though, we had several micro-services, each having its own GitHub repository. We followed the share (almost) nothing principle, besides having another repo for protobuf files for inter-service synchronous gRPC and asynchronous event based communication. Our team consists of four developers, or rather enterprise solution architects with 10+ years of experience, but this is worth another post, how such a team performs! We all are owners of all the codebase and review all PRs. We also write in one language.

Photo by Yoal Desurmont on Unsplash

A new service, a new repo, a new hope

One day we had to create a new micro-service which would end up with yet another repo being created and an additional IDE window being opened and another project having to be pulled regularly. At this point we decided to give the monorepo approach a chance. The main benefits we’ve been expecting were:

atomic API changes, especially convenient at the beginning of development when requirements are still fuzzy and these kind of cross-project PRs are present more often,

all micro-services are always up-to-date and can be run immediately,

one IDE window enabling source code navigation between services and generated java classes from protobuf files,

common build related configurations extracted into a main build file.

Don’t mess with gradle or you will regretle

When you google for monorepo gradle you may find the first few tutorials and articles all talking about Composite Builds. On the gradle blog you can read:

Composite builds are a way to join multiple independent Gradle builds and build them together.

This must be it! But the more you read, the more confusing it becomes. It turns out that although you can setup a monorepo with Composite Builds and it will work and satisfy most of our requirements, it’s not really what we should use in our case. I’ve found a few summaries, where Composite Builds are a good fit, like in this post:

Composite builds are a nice feature that enables some use cases like: Fixing a bug in producer that’s affecting consumer. An iterative approach may be needed to make the final fix. In a traditional approach you may need to make multiple intermediate releases for producer until the fix can be verified in consumer. composite builds shortens the time it takes to iterate and also reduces the risk of outdated snapshot dependencies on a repository. You may want to give monorepo a try but don’t want to change commit history for now. You can “fake” a monorepo by composing all required builds. You want to split a monorepo into multiple repositories but some projects may need to be co-located as if they were still part of the monorepo. Some other use case where having direct access to sources for all interesting dependencies is a must.

Also the official gradle documentation states:

Composite builds allow you to: combine builds that are usually developed independently, for instance when trying out a bug fix in a library that your application uses decompose a large multi-project build into smaller, more isolated chunks that can be worked in independently or together as needed

These do not sound like our scenario. It’s not about the burden to release a new version of a library before we can use it in our project. Also the (temporary) composing of just a subset of existing repos into one build for convenient development is not what we really wanted to achieve.

After playing around with a Composite Build, we ended up with a good old Multi Project Build. It’s all there, what we required for our few micro-services and one common protocol module. We have common build configuration in the root directory, altogether with just one gradle wrapper and other configuration files for GitHub and our IDE. Subdirectories contain tailored build.gradle and sometimes .gitignore files and the source code.

Gimme some code!

Below is our main build.gradle file.