TLDR: Playground Monorepo Template
I keep a playground monorepo as my primary workspace for personal work. The repo is my digital equivalent of a basement toolshed. It is the place where I go to express my ideas. Whenever I want to start a prototype or to test things out, I know exactly where to start writing the first line of code.
My playground folders have slowly evolved to become a monorepo over the years. The reason to use a monorepo for my personal project is similar to why a team or a company would want to migrate to a single repo. I use different languages to experiment depending on the hobby and the experiments I am working on. I used to cmd+tab
, cmd+~
, open, and close way too many editor windows. I want to share codes among different projects. I want to centralize my documentations. I want to have a single editor interface where I feel at home. I used to have many small repos. I would have a hard time remembering which one was which. I spent too much time setting up new repos. Sometimes I spent more time setting up than making progress on them.
A playground monorepo has been a huge success for my personal productivity. I use it for more than just codes. I have organized my noncoding readings and writings into the playground as well. It is embarrassing to say that spending time in my playground sometimes gives me the feeling of working on a well organized basement bike shop with all the handy tools hanging on the wall.
Here is the template that I extracted from my playground monorepo. My repo is much large than what is shown there, but they shared the same basic setup.
A Single Build Tool: Bazel
First and foremost, my playground repo is setup to write codes. In the last few years, I have become a fan of using Bazel as a multi-language and multi-platform build tool. There are many great resources out there explaining its virtues and shortcomings. I am not going to enumerate them here. For my purpose of finding a polyglot build tool for experimental codes, the #1 reason is how little I have to tinker the build tool once I set up a particular programming language. It just works, even when I just git clone my playground in a new computer.
When I first decided to setup my playgrounds as a monorepo, all the outdated build tools in my old repos made me cringed. I forgot how to build and run the projects I created. Part of the motivation of putting everything in Bazel was my belief that Bazel build files were less likely to get outdated by yet another monorepo build tool. I didn’t want to keep updating my monorepo build tool for each language every year. Many build tools change so quickly that it is tiring to try to keep up. Bazel is sufficiently mature and sophisticated that it is not likely to go through that kind of dizzying updates.
Support Multiple Languages
Before my playground monorepo, I had to setup every time I started something new. The most annoying part about setting up a new repo was the feeling of deja vu. I would get this sense that my world is glitchy. The second most annoying part is that I always had an urge that I need to apply the best practices of the latest build tool of the language that I am working on. That would easily lead me down a rabbit hole. I only wanted to spend as little time setting up as possible. When I have ideas, I want to write.
I write in many languages. Sometimes it is beause there is the right tool for the job, but a lot of time, I am playing around and it is more fun to choose a stack that I want to experiment with. Building in Bazel offers similar experiences in defining library and binary targets regardless of the language of choice.For example, here are two target definitions
# go
go_library(
name = "go_binary",
srcs = ["main.go"],
importpath = "github.com/jinfwhuang/playmono/cmd/helloworld",
)
# python
py_binary(
name = "py_binary",
main = "main.py",
srcs = ["main.py"],
)
Furthermore, executing these binaries are exactly the same.
bazel run :go_binary
bazel run :py_binary
It removes a lot of mental switching cost when I shift from working in one language to another. Often, the highest cost in context switch is understanding the build tool and debugging environment. The programming languages and the ideas expressed in codes are usually intuitive and self explanatory. For me, a good analogy is with natural languages. I am more or less in autopilot when I switch talking to my parents in Chinese and talking to my wife in English when we are all in the same conversation together. However, trying to cook my normal dishes in my parents’ all Chinese kitchen is really awkward. My parents’ kitchen doesn’t have my usual spices; it is hard to find an appropriate spatula; the pots and pans are all the wrong shapes and sizes. They have issues with my kitchen setup as well. It is not a perfect analogy. Most of the time, Bazel is a superior build tool than each of the language’s best alternatives. It is like having a world class kitchen that is designed for both American and Chinese cookings.
Monorepo as a Point of Centralization
The greatest benefit of a monorepo is centralization. I can easily reuse my codes across projects. I don’t have to copy and paste. I don’t have to publish to Git, Maven, NPM, or PyPI just so I could consume it in a different project. Maybe a bit overlooked, but equally important is the feature that I could easily review and search all of my codes and notes in the same repo. Because it is not a shared codebase, I put a lot of unedited, completely disorganized notes right next to my playground codes.
Beyond coding, my other personal computer time is spent on reading and writing. I have always kept a chronological order of folders to house the readings that are in PDFs. Back to my school days, most of the documents I produced were text and latex files. Nowaday, I more or less write markdowns exclusively. This blog is written in markdown. Even some of my email drafts were written in markdown in my playmono repo before they were copied into Gmail. I am so much more comfortable writing in my playground so that I do almost all of my writing tasks inside my playground IDE. This feature was a basic directory structure that adopted from how I used to organize my documents.
$ tree -L 1 research/
research/
├── 2010
├── 2011
├── 2012
├── 2013
├── 2014
├── 2015
├── 2016
├── 2017
├── 2019
├── 2020
├── 2021
├── README.md
├── project1
└── project2
Infrastructure
I don’t deploy my toy projects often, but at any given point, I probably have at least a few services running in AWS. I stick my infra codes here in the monorepo as well. For example, see this basic directory structure.
Bazel rules_docker makes the transition from building binaries to images seamless. When a binary is defined in Bazel, creating an equivalent distroless image is too easy. Furthermore, rule_gitops is used to connect docker images and deployment files. See an example of deployment. While this feature is not used often in my personal playground, the overhead is low enough that I had converted my previous services deployed in EC2 into this deployment structure. I keep a tiny k8s cluster running in AWS. Once that was setup, instead of having to deal with terraform and ansible to deploy toy projects, I would deploy to my personal k8s cluster by one bazel command. For example,
# Build and push docker image, create deployment manifest, and deploy to k8s
bazel run //deployment/helloworld:helloworld-namespace1.apply
Configure Laptops
My monorepo also configures my personal macbook laptops. I use ansible to keep track of my dot files, application configuration files, and most of the system applications that could be installed through command lines. I used to keep that in a separate repo. I consolidated the setup into my monorepo. Here is an example structure.
$ tree -L 1
.
├── dotfiles
├── hosts
├── main.yml
├── playbooks
├── roles
# sync all dot files
ansible-playbook -i hosts main.yml --tags dotfiles
# full sync
ansible-playbook -i hosts main.yml
This allows me to version control my dot files and automate setting up my laptops. I might write a detailed post about how I implement this feature.