---
title: "Large Codebase Patterns"
slug: "large-codebase"
category: "file-organization"
tags: ["file-organization", "large-codebase", "codeowners", "modular-monolith", "microservices"]
status: "stable"
last_updated: 2026-05-14
summary: "Patterns above 100k LoC: CODEOWNERS at folder level, modular monolith vs microservices, build sharding, decompose by data ownership."
related: ["[[file-organization/monorepo]]", "[[file-organization/folder-hierarchy]]", "[[file-organization/project-structure]]", "[[coding/git]]", "[[tooling/github]]"]
---

## Overview

Past about 100,000 lines of code, the patterns that worked at 10,000 stop scaling. Every developer steps on every other developer; CI takes too long; nobody owns the messy bits. The rules below cover the structural moves that buy back legibility: ownership at the folder level, the modular-monolith vs microservices decision, decomposition by data, build and test sharding, and import paths that survive IDE indexers. They build on [[file-organization/folder-hierarchy]] and [[file-organization/monorepo]].

## Decompose by data ownership, not by technical layer

The biggest decomposition mistake at scale is splitting by layer (`web/`, `api/`, `workers/`, `db/`). Every feature change touches every layer and teams collide on every PR.

Decompose by data ownership instead. The team that writes a `Subscription` row owns the model, the API that exposes it, the worker that processes it, and the migration that evolves it.

- **Bad:** `payments-api` + `payments-worker` + `payments-db-migrations` as separate codebases owned jointly.
- **Better:** a `payments/` module that owns its API surface, background jobs, and schema end to end.

The team boundary tracks the data boundary. Conway's Law works whether you want it to or not.

## CODEOWNERS at the folder level

A 100k-LoC repo without CODEOWNERS turns every PR into a guessing game. List ownership per top-level module:

```
# .github/CODEOWNERS
/packages/payments/         @org/payments-team
/packages/identity/         @org/identity-team
/packages/notifications/    @org/growth-team
/infra/                     @org/platform-team
/.github/                   @org/platform-team

# Default fallback
*                           @org/maintainers
```

Rules that hold up:

- One folder, one owning team. Joint ownership creates the same diffusion of responsibility that no-ownership did.
- The default fallback (`*`) catches new files. Without it, untouched paths have no reviewer.
- Required reviews from owners are enforced via [[tooling/github-branch-protection|branch protection]] (see [[tooling/github]]).

## Modular monolith first, microservices when forced

Reach for microservices only when a concrete constraint forces them: an independent deploy cadence, a workload that needs different scaling, or a regulatory boundary that needs process isolation.

Default to a modular monolith with strict internal boundaries:

- One deployable artifact, many internal modules.
- Modules import each other through public interfaces only (see [[file-organization/folder-hierarchy]]).
- A single migration story, a single observability story, a single deploy.

A modular monolith preserves the option to extract a microservice later. The reverse is much harder. See [[file-organization/monorepo]].

## Enforce module boundaries in tooling

Convention is not enough at scale. Wire boundaries into the toolchain so a cross-module shortcut fails CI.

- TypeScript: ESLint `no-restricted-imports` or `eslint-plugin-boundaries`.
- Python: a custom `ruff` rule, or a `pytest` test that imports every module and asserts the graph.
- Go: `internal/` is compiler-enforced; lean on it.
- Java: `ArchUnit` tests for layer rules.
- Build graph: Bazel or Nx refuse to build if a `visibility` constraint is violated.

A boundary that lives only in a wiki gets crossed within a quarter.

## Shard the build and the test suite

A 30-minute CI run kills productivity. At 100k LoC, sharding is mandatory.

- **Build sharding.** Only rebuild modules whose inputs changed. Turborepo, Nx, Bazel, and `gradle --build-cache` support this. A CI run that does nothing should finish in under a minute.
- **Test sharding.** Split the suite across parallel CI workers via [[tooling/github-actions|GitHub Actions]] matrix, pytest-xdist, Jest `--shard`, or Go's `-parallel` flag.
- **Affected-only on PRs.** PR CI runs tests for modules the diff touched; nightly CI runs the full suite.

Build a cache hit-rate dashboard. A cold cache on every PR means the cache is misconfigured.

## IDE-friendly imports

At 100k LoC, IDE indexing performance is a feature. Import style affects it directly.

- Use absolute imports from a configured root (`@org/payments/...`), not deep relatives (`../../../../payments/...`).
- Avoid barrel files at the module edge. They wreck tree-shaking and confuse "go to definition."
- Per-module [[coding/typescript-tsconfig|`tsconfig.json` with `references`]] lets TypeScript build incrementally.
- For Python, `from billing import invoice` reads better than `from billing.invoice import *`.

Slow IDE means slow developer. Fix the indexer with the same urgency as a slow test suite.

## Migrate in linear steps, not flag days

When a 100k-LoC schema or API change has to land, the temptation is to coordinate a flag day. Do not.

- Land the additive change first (new column, new endpoint). Deploy.
- Migrate readers and writers incrementally. Deploy each.
- Remove the old path last. Deploy.

Each step is its own PR with its own owners. Feature flags live in a known location; migrations live in a numbered directory; deprecated paths carry a "remove after <date>" comment. See [[coding/git]] for the commit hygiene that keeps the trail readable later.

## Related

- [[file-organization/monorepo]]
- [[file-organization/folder-hierarchy]]
- [[file-organization/project-structure]]
- [[coding/git]]
- [[tooling/github]]
