Skip to content

I always wanted a monorepo

I finally ended up with the monorepo I always dreamed about, through an organic evolution of my projects.

Published at: 2025-01-05

If you've read any of my posts, you'll know that I have a tendency to reminisce about the past. When I implement a project that I remember dreaming about years before, it satisfies me like no other.

This is another one of those stories.

C/C++ monorepos

I love the idea of a complex mega-project for "systems" (or low-level?) languages (think C, C++, Rust, ...). Image something like this:

$ ls -l ~/fake-monorepo
client
server
shared
thirdparty
datastructures
algorithms
util
scripts

Something where each of these pieces unlocks the other, where the whole disgusting mess of Bash and Python scripts, Makefiles, CMakeLists.txt files, etc. produces something larger than the whole.

I think generating artificial complexity that makes sense to the creator is one of my biggest satisfactions when coding. It's also not synonymous with quality - in fact, I'd posit the opposite. Complexity for the sake of complexity is insane. Why would you want to take something useful and straightforward and smash it into a million pieces?

Another observation that resonates with me is that programmers deep down like to reimagine and reinterpret problems with their own abstractions, like building legos. It's a known phenomenon that we like to write code more than we like to read it, and I think part of the reason is that we don't want to learn another person's abstractions: we want to create our own.

The art of breaking useful things into a million garbage pieces

Let's take a look at Docker, one of the exemplars of this phenomenon. Docker, when it came out, was quite dope (I still think it's dope). Of course the grumps come out of the woodworks talking about "BSD always had jails" - to me, might makes right, and the fact that BSD jails were nowhere to be seen in mainstream adoption for software infrastructure (whether for building or deployment) means that Docker's success at popularizing the same idea stands as a unique achievement which places it above whatever existed before it.

What does Docker look like today? Containerd, cgroups (v1? v2?), runc, crio, OCI. It's probably not as I say "complexity for the sake of complexity" but an inevitable way of breaking up a problem into subproblems, allowing each part to be improved and progress independently - think services vs. microservices.

My first attempts

One of my stickiest open-source project ideas (shared by many people, by the looks of it on GitHub) is my own collection of algorithms and datastructures, part of the shared anxiety around whiteboard interviews and the common knowledge of "lmao if you know the fundamentals you'll make $800k USD per year in RSUs" attitude.

I wanted to make a monorepo of algorithms and datastructures, entirely internally consistent. I would write the low-level implementation by hand in C, and then implement my own Python wrapper around it. Then, to prove that I truly understand the behavior of lists and arrays like foo = ['a', 'b', 'c'], I would have implemented the underlying contiguous memory allocations myself.

The working project name was either "monk" or "monastery", probably with the implication that I shed all my wordly beliefs and possessions and give myself up to the absolute purity of Computer Science 101, like a religious monk in some mountain.

Serious career advice

Undeniably, spending years trying to egotistically write my own faster versions of existing algorithms and datastructures has at least led me to be a much better customer of external libraries.

Want to work on "memory optimization"? Instead of writing a hash map from scratch, rewrite your shitty O(N^2) Python script where you're using for loops with NumPy ndarrays or PyTorch tensors which could be replaced with a built-in operator that are 10000x faster. Don't think about writing fast code, think about responsibly using external libraries by adapting your inputs to their patterns and benefiting from their strong points.

Your mind has a tendency to leap to an incorrect conclusion (I have to rewrite xyz from scratch) whereas it might already be a solved problem using a thirdparty library, a Linux kernel option, or even a function or parameter in the existing code you're already using. Implementation starts with research. Like Knuth says, throw away your first 20 prototypes, etc. I'm not sure he ever said that but he's said some really great things..

Probably the best example of this is demucs.cpp vs. demucs.onnx, where my handwritten palace of dreams and deception is beat by a factor of 5x by slightly rethinking about my problem to be able to fit a professional-quality, industry leading AI inference library ONNXRuntime (which when you dig into its rich ecosystem of execution providers, can probably reach up to 5000x faster on specialized hardware like GPUs).

I used to think about going to a cabin in the woods and rewriting demucs.cpp to lovingly optimize each line of code. Instead, over the course of a weekend, I made a speedup bigger than I could ever writewith my own hands by using ONNXRuntime.

The natural monorepo

When I finally stopped writing code for the sake of writing code, and instead simply aimed to solve my own problems, and did that for a few years, I ended up with the beginnings of a product (https://freemusicdemixer.com). I've written about it elsewhere but it's essentially a website for music stem separation which uses the deep learning (aka AI) model Demucs.

I used the same core C++ code for Demucs inference in the website, and copy-pasted it around in a few different places. In 2025, I want to publish more related projects (e.g. mobile apps, etc.).

Multi-platform frameworks vs. native per-platform

One challenge I have is rewriting or re-implementing similar things for different platforms. For example, I need a way to pass in an audio or music file that works on my website, on Android, on iOS, etc.

The best thing I can probably do is move as much of the logic of a stem separation website as possible into C++. That's because the same C++ core can be compiled to target WebAssembly and then called via Javascript from my website, it can be compiled into an Android or iOS native library which can then be called by the frontend application code (in Kotlin or Swift), etc.

The alternative is to use a multi-platform framework like React Native, Flutter, etc. I think those lead to me needing to learn both the framework and the native platform for exceptions, performance, etc. I'd rather just learn the native platform for the presentation layer, and move as much things as possible down to C++ to share.

So, voila, I have a monorepo now, and it's every bit as gnarly and complicated as I imagined, but it's extra beautiful because of how it arose organically. I have:

  • Scripts to convert the PyTorch AI deep learning models to ONNX models
  • Scripts to then convert those to ORT (ONNXRuntime), simplify, quantize the networks to make them smaller and faster
  • Scripts to compile minimal versions of ONNXRuntime to only support the operators and types used by my AI models
  • Shared C++ source code that implements a cross-platform core of operations such as resampling audio files, running the Demucs stem separation AI model, running the MIDI converter and automatic music transcription C++ code, etc.
  • C++ source code for different target applications or platforms including generating WASM modules, Android libraries, command-line apps for my own debugging purposes
  • Scripts to run basic test cases to ensure all of the code is working
  • CMakeLists.txt files to compile all of the different C++ code
  • Makefiles to coordinate cmake commands

Finally, I do have separate repos for each frontend product (app, website, etc.), which uses native code compiled from my monorepo.

Comments