Skip to content
AXIYANA Technologies
Delivery//6 min

A monorepo shape that survives a small team

Monorepo advice is mostly written by companies with a platform team. Here is what is actually worth adopting when there is no such team.

Most monorepo tooling exists to solve problems that appear at hundreds of engineers: remote build caches, dependency graphs across thousands of packages, code owners per directory. Adopting that machinery on a team of five buys you the maintenance and none of the benefit.

But the underlying reason for a monorepo — one commit changes the API and its consumer together — is worth just as much to five people as to five hundred. The question is how little you can adopt and still get it.

What earns its place

  • One repository, one install, one lockfile. A checkout is complete: a new engineer runs one install and everything works, and a pull request that changes a shared type and every call site is one reviewable unit.
  • Workspaces from the package manager you already have. npm, pnpm and yarn all do this. You do not need a build orchestrator to have workspaces.
  • A shared types package, and almost nothing else shared. Types are the highest-value thing to centralise because they are what drift silently.
  • Typecheck and lint across the whole repo in CI. This is what makes the arrangement pay: a change that breaks a consumer fails before merge, not in an unrelated deploy a week later.

What does not

  • A build orchestrator, at first. Remote caching and task graphs solve minutes of CI time. On a small repo CI is already short. Add one when a full run genuinely hurts, and you will know when.
  • Per-package versioning. Versioning internal packages against each other reintroduces exactly the coordination cost the monorepo was meant to remove. Internal packages track the commit; only published artefacts get versions.
  • Directory-level ownership rules. With five people everyone reviews everything, and codifying ownership at that size mostly produces stale rules.

The shape

apps/
  web/          the product
  admin/        internal tooling
packages/
  types/        domain types, shared everywhere
  config/       tsconfig, eslint, tailwind presets
  db/           schema, migrations, generated client

Two rules keep it honest. Apps may depend on packages; packages may not depend on apps. And a package earns its existence when a second consumer appears — not in anticipation of one.

The failure mode to watch

The predictable way this goes wrong is a shared or common package that accumulates everything with no boundary. It starts as two helpers and becomes the thing every other package depends on, which means every change rebuilds and retests everything — the coupling you built the structure to avoid.

Name packages after what they are for. If you cannot name it precisely, it is not a package yet; it is two things waiting to be separated.

When to reconsider

This shape stops fitting when full CI runs get long enough that people start skipping them, or when two apps need genuinely different release cadences. Both are legible signals. Neither is a reason to adopt the machinery in advance.

Start a conversation

If this is the kind of reasoning you want on your product, tell us what you are building.