You open a PR. GitHub looks at the changed paths, checks them against CODEOWNERS, and auto-assigns a reviewer. This happens so often you probably don't think about it anymore. It's infrastructure — it just works.
Except sometimes it doesn't, and it fails in a way that never throws an error.
Here's a realistic example:
# CODEOWNERS
/payments/ @engineer-a
Engineer A hasn't touched payments/ in eight months — they moved to a different team in Q1. Engineer B has made the last 18 commits in that directory and has reviewed most of the recent PRs that touched it. Engineer B isn't in the file.
Your PR touches payments/refunds.go. It gets routed to Engineer A.
Nothing is misconfigured. The glob pattern matches. The username resolves to a real, active employee. GitHub does exactly what it's supposed to do. The routing logic is just missing context it was never designed to have.
This is the gap this article is about: CODEOWNERS tells you who owns a file. It doesn't tell you who should review a specific change. Those turn out to be different questions, and the difference gets bigger as your team and codebase grow.
1. What CODEOWNERS Gets Right
Before picking it apart, it's worth being clear that CODEOWNERS solves a real problem well, and nothing below is an argument for ripping it out.
- Explicit ownership. Anyone can look at one file and see who's accountable for a given path. No tribal knowledge required.
- Required reviews. You can enforce that certain paths always get eyes from the right team before merge — critical for things like auth, billing, or infra-as-code.
-
Clear accountability. When something in
/payments/breaks, there's no ambiguity about who's supposed to be paying attention to that code. -
Simple, predictable behavior. It's a text file with glob patterns. No ML model, no black box, no surprises. You can
git blameit.
For a small team, or for stable, slow-moving parts of a codebase, this is close to sufficient. The problem shows up specifically as team size and code churn increase — which is exactly when the cost of getting routing wrong also goes up.
2. Where Static Routing Breaks
CODEOWNERS answers one question — who owns this path — and organizations often treat that as a proxy for a different question: who should review this change. Here's where those diverge:
| Signal | Static CODEOWNERS | Dynamic routing |
|---|---|---|
| File ownership | Yes | Yes |
| Recent contribution | No | Yes |
| Current expertise | Limited | Yes |
| Review history | No | Yes |
| Reviewer load | No | Yes |
| Expertise decay | No | Yes |
Walking through the "No" column:
- Recent contribution — CODEOWNERS has no time dimension. An entry from two years ago carries the same weight as one from yesterday.
- Current expertise — "Limited" because ownership is often a reasonable proxy for expertise at the moment the file was written, but it's frozen after that. It doesn't update when someone's knowledge of an area grows or fades.
- Review history — the file doesn't know who's actually been reviewing changes in an area, only who's nominally responsible.
- Reviewer load — https://www.codzee.io/will happily assign a fifth simultaneous PR to someone already buried in review requests. It has no concept of "currently busy."
- Expertise decay — there's no mechanism for an owner's relevance to degrade over time as they stop working in an area, and no mechanism for someone else's relevance to rise as they start.
None of this is a criticism of CODEOWNERS as a tool — it's just outside its scope. It's a static ownership file, not a reviewer-relevance model. The mistake is using it as if it were the second thing.
3. Model Expertise as Something That Changes
The core idea: expertise isn't binary, and it isn't permanent. "Alice owns payments" is a snapshot. What you actually want to know is closer to "how relevant is Alice's expertise, right now, to this specific diff."
Useful inputs for that:
- Last contribution date — when did this person last touch this area?
- Number of recent relevant changes — not lifetime commits, recent ones. Someone with 3 commits last month is a stronger current signal than someone with 200 commits two years ago.
- Similarity to the current PR — has this person touched the same files, the same functions, the same data model recently?
- Review activity — have they been reviewing PRs in this area, even if they haven't authored changes there?
- Historical ownership — were they ever formally responsible for this, even if the file has since changed?
You can express this as a rough conceptual model — not a formula anyone should treat as an industry standard, just a way to make the tradeoff explicit:
reviewer_relevance = ownership_weight
+ expertise_weight
+ recency_weight
- load_penalty
The actual weights are going to be organization-specific, and honestly team-specific within an org. A security-critical path might want ownership weighted heavily no matter what. A fast-moving product surface might want recency and review history to dominate. The point of writing it this way isn't to hand you a formula — it's to make explicit that "who reviews this" is a function of multiple decaying/growing signals, not a single static lookup.
4. Work Through a Sample PR
Say you've got a repo with three top-level domains:
/payments/
/identity/
/notifications/
A PR comes in touching payments/refunds.go. CODEOWNERS says payments/ belongs to Engineer A. Here's how three candidate reviewers actually stack up:
| Reviewer | Ownership | Recent work | Similar reviews | Current load |
|---|---|---|---|---|
| A | High | Low | Medium | High |
| B | None | High | High | Medium |
| C | Medium | Medium | Low | Low |
- Engineer A is the listed owner, but hasn't done recent work in this directory, and is already carrying a heavy review load.
- Engineer B isn't in CODEOWNERS at all, but has been actively working in this area recently and has reviewed similar changes — they've probably seen the exact class of bug this PR could introduce.
- Engineer C has moderate ownership and moderate recent activity, low similarity to this specific change, but has bandwidth right now.
A purely static system routes to A every time, because A is the only name it knows how to check against. But on the actual signals that predict a useful review — recent, hands-on context and demonstrated familiarity with similar changes — B is the stronger candidate, and C is a reasonable fallback if B is unavailable.
This doesn't mean A should be removed from the review entirely — for something in a sensitive path, keeping the formal owner in the loop for accountability might still make sense. It means A shouldn't be the only signal the system checks.
5. A Practical Routing Framework
Distilling this into something you could actually reason about (or build) for your own repo:
- Ownership — who's formally accountable, per CODEOWNERS or equivalent?
- Recency — who's touched this code lately?
- Expertise — who has a track record of relevant, substantive contributions here?
- Review history — who's reviewed similar changes before, and reviewed them well (not just approved fast)?
- Load — who has the bandwidth to actually do this review in a reasonable timeframe?
None of these should unilaterally win. Ownership keeps governance intact — you don't want a recency model quietly routing security-sensitive changes away from the team accountable for them. Load is a modifier, not a primary filter — someone who's slightly busy but the clear best fit is often still the right call over someone who's fully free but has no context. The five signals are meant to be combined, with the weighting depending on what kind of change you're looking at (a one-line config tweak needs way less of this than a cross-cutting refactor).
6. The Routing-Decay Loop
Here's the pattern that causes CODEOWNERS files to quietly go stale, laid out as a loop:
People change teams / roles
↓
Code changes (new services, refactors, deprecations)
↓
Expertise shifts (some people gain context, others lose it)
↓
Ownership file stays the same (nobody's job to update it proactively)
↓
Routing quality declines
Nothing in that loop is anomalous — it's just normal organizational and codebase churn. The failure isn't that people move around or that code changes. The failure is that CODEOWNERS has no way to hear about any of it. It's a file, not a feed.
The fix isn't "update the file more often" — that's a losing battle against how fast real orgs change. It's closer to: continuously derive dynamic signals from data you already have (commit history, PR review history, recent activity) and layer them on top of the static file, rather than trying to keep the static file itself perfectly current. Contribution and review data refresh automatically as people work — no one has to remember to edit a YAML-adjacent file when Bob becomes the de facto payments expert. The signal is already sitting in your git history and PR review logs; it's just not being used for routing.
7. What Teams Can Measure Today
You don't need new tooling to start getting visibility into how well your current routing is actually working. Most of this is derivable from data already in GitHub:
- Reviewer reassignment rate — how often does the auto-assigned reviewer get swapped out before the PR merges? High rate = your default routing isn't matching reality.
- Time to first meaningful review — not first comment, first substantive review. If the assigned reviewer routinely takes days to engage, they may be overloaded or not the right fit.
- Review load concentration — what percentage of review volume in a given area lands on one person? If it's consistently one name, you likely have a bottleneck, whether or not that person is technically the best expert.
-
PRs routed to inactive contributors — a rough version of this is: for each CODEOWNERS entry, check
git log --since="6 months ago" -- <path>and see if the listed owner shows up. If not, that entry is a candidate for drift. - Review handoffs — PRs where the review gets passed from one person to another before merge are a decent proxy for "the first routing decision missed."
- Recent contribution coverage — what share of merged PRs in an area were reviewed by someone who'd actually committed to that area in the last N weeks?
None of these require a new system to start tracking. A few are a git log or a GitHub API query away. The value isn't in any single metric — it's in noticing, over a few months, whether your routing is drifting in a direction nobody's paying attention to.
8. Practical Takeaway
None of this is an argument for dropping CODEOWNERS. It's still the right mechanism for encoding accountability — who's on the hook for a given part of the codebase, and which teams need guaranteed visibility into changes that touch their area.
What it's not sufficient for, on its own, is figuring out who should actually review a given PR today.
Keep CODEOWNERS for accountability. Add dynamic signals for relevance. Ownership answers "who's responsible." Recency, expertise, review history, and load together answer "who's actually the right person to look at this, right now." A routing setup that only asks the first question will keep sending PRs to people who used to be the right answer. A routing setup that ignores the first question entirely gives up the governance guarantees CODEOWNERS exists to provide. You want both.
How are you handling reviewer routing today — static CODEOWNERS, team rotation, tribal knowledge ("just tag whoever's around"), or something more dynamic? Curious what's actually working at different team sizes — drop your setup in the comments.
Top comments (0)