A brief note on monorepos and why they’re an anti-pattern for .NET (and Java, and similar languages hosted on a runtime).

Monorepos exist to address the development and deploy problems that result when the following things are true:

  • the product is a monolith
  • libraries or similar external dependencies are distributed as source code

That last is true both for scripting languages and languages that have to be compiled to native machine code, because for both types of languages there’s no way to distribute a compiled universal binary.

However.

.NET (and Java, et. al.) do allow you to distribute a universal binary in the form of versioned bytecode DLLs in NuGet packages, and that’s the Right Way To Do It.

Monorepos derive from the Node/JS ecosystem because JavaScript is a scripting language and even though there’s a defined package format, Node has no standard library. This leads to great proliferation of packages to do things as simple as reversing a string. That mentality bleeds over into (often distributed) monoliths, where code that’s rapidly changing is used in multiple components, and the nature of the JavaScript bundling process means it’s much easier to simply consume such code as source.

Similarly, languages like Rust or C++ that compile to native machine code have to be compiled for the machine architecture they’re going to run on. There’s little reason to put shared internal code in a source package and go through the version - publish - consume - build cycle when you can just stick that code in a parallel directory.

.NET and Java don’t work like that. You can - and should - publish your shared code as versioned, compiled NuGet packages so you can semver your interfaces. This allows you to break the coupling between different components so they can be deployed separately, and to separate your code by domain. These are all good things; they reduce the cognitive load on your developers and make it easier to work on parts of the code without having to keep a huge monolith in your head.

Monorepos tend towards Big Balls of Mud because it’s so easy to crosslink functionality and architecture across multiple directories in a giant repo. Thinking of your code as reusable, versioned libraries that remain within the boundaries of their domain keeps your products modular and easier to deploy.

Oh, and one last thing: if you have two (or more) components and they both depend on some shared code, and that shared code churns so fast it’s onerous to put it in a library - you don’t actually have two+ components. You have one component that you’ve spread across multiple projects. Either consolidate it or break it down by domain.