esence.io

Cover Image for the blogs post
·platform, monorepo, codebase, caching, library

Scaling Codebases Without Platform Bloat

Smart Caching and Frugality in Monorepos for High-Velocity Teams

Redundant compilation lags, bloated CI pipelines and version drift for internal modules quietly drain developer velocity. And normally, it's too late when the leadership gets the whiff of it. According to recent studies, the hidden tax on your team looks exactly like this -

  • The Direct Time Drain: Your engineers waste an average of 25 to 50 minutes due to compilation lags.

  • The Redundant Build: An engineer wastes his valuable time rebuilding something that already exists and is being used in another team.

  • The Pipeline Blocker: Your Frontend Team spends an entire sprint cycle completely idle, waiting for the Core Platform Team to version-tag and publish the latest approved button changes.

All these, as bad as they sound, are common occurrences in the majority of software engineering teams trying their best to ship fast and iterate faster. The problems that I listed are not inevitable, and can be solved if we just carefully design our engineering culture and the codebase that it revolves around from the beginning.

In today's edition, let's tackle them one-by-one and continue where we left off last time in our adventure of building the ideal monorepo setup for startups.

Assuming that you followed my advice of having a monorepo for all your codebase needs for your startup, let's hit the road!

The Redundant Build: A guide to module re-use

While there's no sure-shot way of knowing what an engineer thinks before embarking on the painstaking journey of rebuilding the wheel, we can mitigate the risk through some primary cautions -

  • Build a collection of re-usable modules.

  • Business logic must never be allowed to be repeated.

  • Engineers should learn about the existing system for their use-case.

Though I know, forcing software engineers to do something they don't approve of is never a good idea. But Companies and Team Leads should force these elementary coding principles rigorously via code-reviews and documentation.

Building a Collection of Reusable modules

Startups that successfully keep growing, also tend to constantly rebuild many parts of their system again and again. To give you a few examples -

  • UI Components: The same kind of button, the same navigation bar, the same sidebar nav, etc. are rebuilt across different product lines to keep the look and feel of the brand consistent. The website may be using the same color scheme as the product dashboard, and hence the components get duplicated.

  • Backend Modules: If you have more than one backend, chances are that you are duplicating the authentication, the logging, the middlewares, the utility modules and many others depending upon your particular use-case.

Due to this, some parts of your website might look and feel different than the other parts, worst-case it might feel they are built by different companies altogether. And it's not just about how the UI looks, if you have duplicated backend code, your team sometimes might end up debugging an edge-case over a weekend that an engineer didn't think, while rebuilding the authentication pipeline all over again.

To best avoid the above duplication, is to use the packages directory in your monorepo root judiciously. That is if you followed the monorepo structure that I advocated in the previous edition.

And if you are already doing that, it's better to keep the reusable modules kept inside the packages directory - loosely coupled, highly parameterised and customizable through environment variables and feature flags.

Consistent Business Logic

If there's one thing that you should absolutely avoid, then it has got to be business logic duplication. Given enough time, codes change, bugs appear and get fixed, engineers might come and go, but hardcore business logic seldom changes.

To give you an example, if you are building an accounting system, then your tax calculation logic must never be repeated. If it does get repeated across your backends and your frontends, then each update to the tax calculation logic, debugging sessions and eventual hot-fixing, would need to be made across the board and be forced to be compatible in each of the environments, causing huge business consequences

I would like to recommend keeping a separate privileged module inside your shared-modules, for such sensitive business logic and have a CODEOWNERS entry such that unauthorised engineers (most likely the interns) can never make any changes to the same.

An example CODEOWNERS entry might look something like this:

# .github/CODEOWNERS
# Guarding core business logic from unauthorized modifications
 
# Global fallback auditors
* @esence-io/platform-leads
 
# Strict guardrails for sensitive domain algorithms
/packages/domains/billing/tax-calculator.ts       @esence-io/finance-architects
/packages/domains/appointments/compliance.go     @esence-io/medical-directors

Documentation for Reusable Modules

This might be the only thing that separates an average organization from a great organization that has some degree of foresight.

However, in an early-to-mid-stage startup, telling engineers to spend hours writing long-form documentation on a separate Confluence or Notion workspace is a fantasy. It creates out-of-date documentation platforms that nobody trusts, adding to your organizational bloat.

The frugal, high-velocity alternative is Git-Adjacent Documentation:

  1. Workspace READMEs: Every package inside your /packages/* directory must contain a minimalist README.md explaining its API footprint, schema rules, and runtime assumptions. If an engineer alters code logic, they update the text in the exact same git commit string.

  2. Strict Typing: Let your compiler act as your documentation hub. By enforcing explicit input/output interface boundaries on shared components, your IDE automatically tells the frontend engineer exactly what parameters a component expects without them ever leaving their workspace terminal window.

  3. Better Code-Comments: Come-on, no one writes good comments.

If code isn't discoverable from inside the repo editor, it doesn't exist. Keep it in git and the engineers will follow.

Trunk-Based Development: Unblocking the pipeline

What if every engineer works on the latest changes produced by every other team? And what if every team starts taking ownership of the changes they make?

To answer both the questions in a strong Yes, your engineering team has to follow the paradigm of "Trunk-Based Development".

Trunk-Based Development as opposed to module-versioning and importing foreign modules from registries, means that a monorepo contains everything it needs and every module it has, is on the latest version. And, cross-module dependencies just become a question of either importing those modules or their DLLs or binaries, completely bypassing the network-overhead of fetching latest modules.

There are no version numbers to increment, no private registries to maintain, and no upstream breaking changes hidden behind a semver tag. And no wasteful standup meetings discussing whether a change is breaking enough to bump up the version entirely.

Once your company starts following trunk-based development:

  • If the Core Platform UI Team changes the button style or its parameters, it's going to be the Core UI Team's responsibility to change it across the codebase. Thereby, creating implicit ownership of all the changes that break integration tests or that make other project's compilers scream.

  • The cost and complexity of infrastructure behind private module/package registries instantly become zero and setting up the local environment for the new joinee just becomes executing the build commands.

  • If a shared or an inter-team module breaks, no one has to get blocked for getting fixes and updates.

  • All teams to some extent become software/module testers for all the other teams by becoming a direct consumer with a zero-lag feedback loop.

One easy way to do it for TypeScript workspaces is by simply including workspace dependencies like this:

 
  "dependencies": {
    "@packages/ui": "workspace:*"
  }
 

It isn't complicated, unless you start making it.

Compilation Lags and Caching Build Artifacts: With Nx

You pay money-tax to the government for earning, and pay time-tax to the compiler for coding. Startups hire accountants for reducing tax paid to the government, but seldom pay any attention to the tax paid to the compiler that slowly leaks their pockets in terms of wasted productive hours of a developer on a payroll.

Let's go over how we can reduce this compiler "time-tax", by first understanding the internal audit system of this time-taxation:

  1. Developers write code and that code needs to be built by a compiler for it to run. For example, your team wrote the code for a website in React, then a bundler comes and transpiles the code into browser primitives.

  2. These primitives maybe the final build output or intermediate ones. And whenever the developers change any code, these primitives are redone by the compiler.

  3. Now pay attention. If you even have a moderate-sized codebase, and engineers constantly making changes, these primitives have to be built by the compiler multiple times, even if they don't need to. Sometimes, they are built and rebuilt hundreds of time in a single day, which takes and wastes the precious time of your best-performing engineers.

To solve this exact same problem, modern software teams tend to use artifact-caching. That is, all the artifacts are cached against the exact code at one particular point of time. And if a code change doesn't directly affect a particular artifact, the compiler re-uses the same artifact from the cache.

On top of these caches, if you setup a central cache registry, where all your build primitives and artifacts are stored, your entire team can access them and benefit from code that gets built even by other team-mates.

Okay, it all sounds magical! But how do we actually achieve it.

There are countless tools that will help you setup the exact same build-cache mechanism. Some are notoriously difficult to reason with like bazel and some are so simple, Nx that they can be setup in half-a-day's time.

Setting up Nx in your monorepo

To add Nx to our existing workspace without introducing a mountain of configuration files, we simply install the core package as a development dependency and drop an nx.json layout file at our root directory.

bun add -d nx

Now, create a minimalist nx.json file in your repository root to define exactly what operations are allowed to be cached:

# nx.json: In monorepo root
{
  "$schema": "./node_modules/nx/schemas/nx-schema.json",
  "targetDefaults": {
    "build": {
      "cache": true,
      "dependsOn": ["^build"],
      "outputs": ["{projectRoot}/.next", "{projectRoot}/dist"]
    },
    "cloudflare:build": {
      "cache": true,
      "outputs": ["{projectRoot}/.vercel/output/static"]
    }
  }
}
 
# project.json: To mark a non-javascript/typescript project
{
  "name": "go-backend",
  "root": "app/go/backend",
  "projectType": "application",
  "tags": ["product:go-backend"],
  "targets": {
    "dev": {
      "executor": "nx:run-commands",
      "options": {
        "command": "go run ./cmd/server/main.go",
        "cwd": "app/go-backend/backend"
      }
    }
  }
}

That’s the entire configuration footprint. Now, when an engineer runs your standardized task build:esence_io:cloudflare command, Nx steps in as an invisible traffic cop. It checks the cryptographic hashes of the files inside the specific application directory and its shared dependency tree.

If you only updated a markdown blog post file inside your marketing domain, Nx realizes the core application code hasn't changed. It skips the compiler entirely, avoids the time-tax, and copies the pre-built files out of your .nx/cache folder in under 100 milliseconds.

And here is the frugality masterstroke for founders and VCs: You do not need an expensive enterprise cloud subscription to share this cache on day one. We can map the .nx/cache directory directly to GitHub Actions caching layer in our delivery pipeline, and your CI runner will populate and pull down the cache automatically. Your engineering team gets a lightning-fast, zero-dollar distributed build system completely for free.

Now with this setup, you would have officially eliminated the 50-minute compiler tax, protected your teams inner development loop, and kept your platform entirely overhead-free.


To discuss more about how we can setup this central cache registry and discuss more about monorepos in general, I will have to write another edition in this monorepo/codebase blog-series, and you would have to wait for maybe a week.

Well, see you then. 👋