DEV Community

KCL Tech
KCL Tech

Posted on

How Monorepos Work: A beginner-friendly guide to monorepos

Written by Waseef Khan, Co-President of KCL Tech.

A codebase rarely stays as one application forever.

Imagine a team building a public marketing site, an internal admin platform and a customer-facing learning platform. Each product has its own users and responsibilities, but they share the same organisation, visual language, authentication patterns and engineering team.

That creates a structural question:

Should every product live in its own repository, or should they live together?

A monorepo can be the most useful answer.

KCL Tech is committed to making technology accessible, so this guide assumes no previous experience with monorepos. I will explain the basic language first, then show how the tools fit together. We will also explore how the same structure can make a codebase easier for AI coding agents to navigate safely.

Five terms to know before we begin

You only need a few ideas to follow the rest of this guide:

  • Repository: A project folder tracked by a version-control tool such as Git. It contains the code and a history of changes.
  • Application: A product people can use, such as a website, mobile app or admin dashboard.
  • Package: A reusable collection of code. One package might provide buttons, while another handles authentication.
  • Dependency: Code that another piece of code relies on. If an app uses a button package, the button package is one of its dependencies.
  • Build: The process of turning source code into a version that can be tested or deployed for people to use.

If those terms make sense, you already have enough context to understand a monorepo. If Git or package managers are completely new to you, the Pro Git introduction and MDN's package-management basics are friendly starting points.

What is a monorepo?

A monorepo is one repository containing multiple applications or packages.

Think of it as one building with several rooms. Each application has its own room and purpose, while shared facilities sit in the same building. The rooms do not become one giant room simply because they share an address.

It is not necessarily one application, one deployment or one giant bundle. A monorepo can contain several independently deployed products:

platform/
├── apps/
│   ├── marketing/
│   ├── admin/
│   └── learning/
├── packages/
│   ├── ui/
│   ├── auth/
│   ├── database/
│   ├── eslint-config/
│   └── typescript-config/
├── design/
├── package.json
├── pnpm-workspace.yaml
└── turbo.json
Enter fullscreen mode Exit fullscreen mode

In this example:

  • apps/marketing is the public website.
  • apps/admin is the internal platform used to manage content, events and programmes.
  • apps/learning is the customer-facing learning platform.
  • packages/ui contains reusable interface primitives.
  • packages/auth contains shared authentication logic and types.
  • design documents how the product family should feel: visual direction, motion, interaction principles, accessibility and content style.

The applications remain separate products. They can have different users, web addresses and release schedules. The repository simply gives their code a shared home.

How the pieces are connected

The folder tree shows us where the code lives. A dependency graph shows us which parts rely on other parts.

Imagine the following relationships:

marketing ──┐
admin ──────┼──> ui
learning ───┘

admin ──────┐
learning ───┼──> auth ──> database
Enter fullscreen mode Exit fullscreen mode

In technical language, each application or package is a node, and each connection is an edge. In plain English, each box is a piece of the system and each arrow means “uses”.

This matters because the graph tells our tooling:

  • what must be built first;
  • which products may be affected by a change;
  • which tests need to run;
  • which previous outputs can be reused from a cache;
  • how many places might be affected by a shared change.

Tools such as Turborepo and Nx use these relationships to decide what work is necessary. The tools differ, but the underlying idea is simple: understand what depends on what before running commands.

How workspace packages connect

We now need a way to tell our package manager which folders belong to the same workspace. A package manager installs dependencies and runs project commands. In this example, we use pnpm.

The root workspace file lists the directories that belong to the monorepo:

# pnpm-workspace.yaml
packages:
  - "apps/*"
  - "packages/*"
Enter fullscreen mode Exit fullscreen mode

Each project still has its own package.json, which acts like an identity card and instruction sheet for that project. An application can list another package from the same repository as a dependency:

{
  "name": "@platform/learning",
  "dependencies": {
    "@platform/ui": "workspace:*",
    "@platform/auth": "workspace:*"
  }
}
Enter fullscreen mode Exit fullscreen mode

The workspace:* value tells pnpm to use the local package from this repository. We do not need to publish @platform/ui publicly just to use it in the learning app.

The result is a clean import:

import { Button } from "@platform/ui/button";
Enter fullscreen mode Exit fullscreen mode

instead of a fragile relative path:

import { Button } from "../../../../packages/ui/src/button";
Enter fullscreen mode Exit fullscreen mode

The first version clearly says, “this app uses the shared UI package”. The second reaches through several folders and makes that relationship harder to understand or control.

How commands run in the correct order

A dependency graph describes which projects rely on each other. A task graph describes the order in which commands such as build, test and lint should run. Linting is an automated check for code-quality and style problems.

For example, the learning app cannot complete its production build until the internal packages it consumes are ready. A minimal Turborepo configuration can encode that relationship:

{
  "$schema": "https://turborepo.com/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "!.next/cache/**", "dist/**"]
    },
    "lint": {
      "dependsOn": ["^lint"]
    },
    "typecheck": {
      "dependsOn": ["^typecheck"]
    },
    "test": {
      "dependsOn": ["^build"],
      "outputs": ["coverage/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

You do not need to understand every line yet. The important one is ^build, which means: build this package's dependencies before building the package itself.

The root package.json then becomes a simple front door to the entire repository:

{
  "scripts": {
    "dev": "turbo dev",
    "build": "turbo build",
    "lint": "turbo lint",
    "typecheck": "turbo typecheck",
    "test": "turbo test"
  }
}
Enter fullscreen mode Exit fullscreen mode

We can run everything:

pnpm build
Enter fullscreen mode Exit fullscreen mode

Or focus on one product:

pnpm --filter @platform/learning dev
Enter fullscreen mode Exit fullscreen mode

A task runner such as Turborepo reads this configuration. It can run unrelated work at the same time, respect the correct order and reuse previous results through a cache. A cache is simply stored work that does not need to be repeated when nothing relevant has changed.

That is what prevents a growing monorepo from automatically becoming a slow monorepo. To go further, Turborepo's guides explain internal packages and package and task graphs in more detail.

What belongs in a shared package?

The easiest monorepo mistake is sharing code simply because two files look similar.

Good shared packages usually represent stable capabilities:

  • design-system primitives such as buttons, inputs and dialogs;
  • authentication clients, session types and permission helpers;
  • database schemas and generated types;
  • analytics wrappers;
  • linting, formatting and TypeScript configuration;
  • utilities with the same meaning across every application.

Code should normally remain inside an application when it represents that product's domain:

  • a learning-platform lesson timeline;
  • an admin moderation queue;
  • marketing-page campaign sections;
  • a workflow that merely happens to resemble another workflow today.

A useful rule is:

Share stable capability, not accidental similarity.

Sharing too early can create an overcomplicated component with too many options, unclear ownership and several products afraid to change it. A small amount of duplication can be safer until the correct shared solution becomes obvious.

One design language does not mean one interface

A shared UI package should contain implementation. The design directory should explain intent.

For a multi-product repository, that directory can stay deliberately simple:

design/
├── README.md
├── foundations.md
├── components.md
├── motion.md
├── accessibility.md
└── content.md
Enter fullscreen mode Exit fullscreen mode

It answers questions that code alone cannot:

  • Should the product feel editorial, playful, technical or institutional?
  • When should motion communicate state, and when would it become decoration?
  • How dense should the admin platform feel compared with the marketing site?
  • Which behaviours must remain consistent across all three products?

The products can feel related without looking cloned. Marketing may be expressive, Learning may be encouraging and progress-led, and Admin may prioritise density and speed. Shared foundations should create family resemblance, not forced uniformity.

A change moving through the monorepo

Suppose we add a new Avatar component to @platform/ui and use it in the learning app.

The flow looks like this:

  1. Add the component and its tests inside packages/ui.
  2. Export it through the package's public API, meaning the approved way other projects can access it.
  3. Import it inside apps/learning.
  4. Run code-quality checks, type checks and tests for the projects that could be affected.
  5. Build the learning app and any other consumer affected by the shared package change.
  6. Deploy the learning app independently.

The shared component and the app using it can be reviewed together in one proposed change, often called a pull request. The products can still be released independently: changing Learning does not require deploying Marketing or Admin unless they are genuinely affected.

What is an agentic monorepo?

A coding agent is an AI system that can do more than suggest a line of code. Depending on the tool and the permissions it receives, it may be able to inspect files, edit code, run commands and check its own work.

“Agentic monorepo” is not a separate build tool or formal repository type. I use it to describe a monorepo deliberately designed so coding agents can understand it, make limited and relevant changes, and verify their own work.

An ordinary monorepo gives an agent access to a lot of code. An agentic monorepo gives it a map, boundaries and a safe route through that code.

That distinction matters. The biggest advantage of a monorepo for an agent is context: the applications, rules, design system and tests are available together. Its biggest risk is also context: an agent working without clear limits could change a shared package and unintentionally affect every application.

1. Give the repository a clear map

Create a root instruction file that explains the repository in plain language. Some coding tools read files such as AGENTS.md automatically. The exact filename is less important than the principle: important repository knowledge should live beside the code instead of only in someone's head.

# Repository engineering guide

## Repository map
- apps/marketing: public website
- apps/admin: internal operations platform
- apps/learning: customer learning platform
- packages/ui: shared interface primitives only
- packages/auth: authentication and authorisation contracts
- design: product-wide design principles

## Commands
- Install: pnpm install
- Run Learning: pnpm --filter @platform/learning dev
- Validate affected work: pnpm lint && pnpm typecheck && pnpm test

## Rules
- Do not import directly between apps.
- Consume shared code through package public APIs.
- Do not place product-specific workflows in packages/ui.
- Never commit secrets or real customer data.
- Update tests when behaviour changes.
Enter fullscreen mode Exit fullscreen mode

This avoids forcing an agent to infer critical architecture from thousands of files.

2. Scope instructions to the work

Root guidance should be short and universal. Product-specific knowledge should live closer to the product:

AGENTS.md
apps/
├── admin/
│   └── AGENTS.md
└── learning/
    └── AGENTS.md
Enter fullscreen mode Exit fullscreen mode

The Learning instructions might explain enrolment rules and customer privacy. The Admin instructions might explain role permissions and audit logging. This keeps the information given to the agent relevant instead of loading every rule for every task.

Agent products support different instruction filenames and scoping rules, so check the tool you use. The architectural principle remains portable: global rules at the root, local rules near the code they govern.

3. Make the correct action the easy action

An agent should not need to invent the setup process or assemble a validation command.

Prefer predictable entry points:

pnpm install
pnpm --filter @platform/learning dev
pnpm --filter @platform/learning test
pnpm --filter @platform/learning typecheck
Enter fullscreen mode Exit fullscreen mode

Also provide:

  • .env.example files containing names, never secrets;
  • predictable sample data containing no personal information;
  • scripts that work without interactive prompts;
  • clear package names and ownership;
  • one command for formatting and one command for validation;
  • fast unit tests plus focused end-to-end tests for critical journeys.

Agents become more reliable when the feedback loop is short enough to run after every meaningful change.

4. Encode boundaries in tools, not only prose

Instructions can be ignored or misunderstood. Architectural rules should also be enforced mechanically.

Examples include:

  • automated code rules that forbid imports from one app into another;
  • TypeScript project references and package exports;
  • required human reviewers for sensitive packages;
  • schema validation at API boundaries;
  • protected automated checks before changes can be merged;
  • permission tests for Admin and Learning;
  • secret scanning and dependency auditing.

An instruction saying “do not bypass the authentication package” is helpful. An automated rule and a failing test are stronger because they actively stop an unsafe change.

5. Optimise for selective context

More context is not always better. An agent working on a Learning dashboard should not need the full Marketing application in its prompt.

A well-structured monorepo allows the agent to inspect:

  1. the root map;
  2. the target application's instructions;
  3. its direct package dependencies;
  4. relevant tests and design documentation;
  5. the affected task graph.

This is one reason clear package boundaries matter beyond speed. They also define what humans and agents need to understand for a particular task.

6. Define the verification loop

Every task given to an agent should end with evidence, not confidence.

A sensible definition of done is:

  • the requested behaviour is implemented;
  • existing architectural boundaries remain intact;
  • linting and type checks pass for affected projects;
  • relevant tests pass;
  • user-facing changes are visually inspected;
  • migrations and environment changes are documented;
  • the final summary names changed packages and remaining risks.

The agent should be able to discover and run this loop from the repository itself.

A practical agent workflow

For a task such as “add role-based progress editing to Learning”, an agent-ready workflow would be:

Read root instructions
        ↓
Read Learning instructions
        ↓
Inspect Learning's dependency boundary
        ↓
Plan the smallest valid change
        ↓
Implement inside Learning where possible
        ↓
Run relevant code-quality checks, type checks and tests
        ↓
Review the affected graph and report evidence
Enter fullscreen mode Exit fullscreen mode

If the work requires changing packages/auth, the task expands deliberately because Admin may also consume that package. The agent should re-check both consumers instead of treating the shared edit as a local implementation detail.

That is the central idea of an agentic monorepo: an agent can work independently, but only within boundaries that everyone can see and check.

Common failure modes

The “shared everything” package

One enormous common package becomes a dumping ground for unrelated code. Prefer packages named after clear, stable responsibilities.

Invisible dependencies

Imports that climb through several folders and duplicated configuration hide relationships. Use named packages, approved exports and automated import rules.

Running every check for every change

This works at first and becomes painful as the repository grows. Run focused checks for the projects a change could affect, while keeping a complete automated test path for high-risk changes and releases.

Treating all apps as one deployment

Repository strategy and deployment strategy are different choices. Each application should have explicit build outputs, environment variables and deployment ownership.

Using instruction files as a substitute for architecture

An excellent prompt cannot rescue circular dependencies, ambiguous ownership or a test suite that does not run. Agents amplify the structure already present.

Should you use a monorepo?

A monorepo is a strong choice when:

  • several products are maintained by the same team;
  • changes frequently cross application and package boundaries;
  • products share types, tooling or stable capabilities;
  • reviewing related cross-project changes together is valuable;
  • consistent engineering standards matter.

Separate repositories may be better when:

  • products have unrelated teams, technology stacks and release processes;
  • access must be isolated at the repository level;
  • almost no code or operational knowledge is shared;
  • independent ownership matters more than cross-project coordination.

Do not choose a monorepo simply because large companies use one. Choose it when your projects already share code, people and responsibilities closely enough to benefit from one home.

Final takeaway

A monorepo is not primarily about putting files together. It is about making relationships explicit.

That means a marketing site, admin platform and learning product can remain distinct while sharing the foundations that genuinely belong to the whole organisation.

Making the repository agentic takes the same idea further. We explain its map, enforce its boundaries, standardise its commands and require proof that changes work. Humans gain a clearer codebase, and agents gain the information they need without being given permission to wander.

One repository can hold many products. The quality of the architecture depends on whether everyone working inside it, human or agent, can tell where one responsibility ends and another begins.

Choose what to read next

You do not need to read every link. Pick the level that matches where you are now.

If the foundations are new

If you want to understand monorepos more deeply

If you want to explore agent-ready repositories

Top comments (0)