Engineering

Five Signals Your Codebase Is Quietly Costing You a Quarter

N
Nivethitha J
May 29, 2026
7 min read
Five Signals Your Codebase Is Quietly Costing You a Quarter

Most engineering teams blame estimates, scope creep, or hiring for missed deadlines. The real culprit is usually buried in the codebase — and it's been there for years.

Every engineering leader I've talked to in the last year tells me the same thing: their teams are shipping less than they used to. They blame the hiring market, hybrid work, AI rollouts, or younger engineers. They almost never blame the codebase.

But when I dig in, the codebase is *almost always* where the time is going. Not in some glamorous "rewrite Stripe in Rust" way — in five quiet, boring ways that compound.

The codebase is the only artifact every engineer touches every day. Ignore it, and you're ignoring the largest input to your output.

A whiteboard mid-architecture-review — the kind of session this post is built for
Architecture reviews are diagnosis sessions, not blame sessions.

1. The PR review queue is the bottleneck, not the typing

Pull up your team's commit graph. If most PRs sit in review for more than 24 hours, and review comments outnumber commits 3:1, your reviewers are doing more work than your writers.

A quick way to check it:

bash
gh pr list --state merged --limit 50 \
--json createdAt,closedAt \
| jq '[.[] | (.closedAt | fromdate) - (.createdAt | fromdate)] | add / length / 3600'

If that prints a number larger than 24, you have a review-throughput problem, not a typing problem.

This isn't a process problem. It's a codebase problem. Reviewers are slow because the changes are hard to verify:

  • Too much context to load — one PR touches auth, billing, and analytics because nothing is properly scoped
  • Too many side effects — changing User.save() updates three caches and fires an email
  • Too little type safety — the reviewer has to mentally run the function to know what comes out
  • Inconsistent patterns — the same problem is solved three different ways across the codebase
  • Hidden coupling — a one-line CSS change breaks a server test, and nobody can predict why

Fix the codebase and review speeds up on its own.

2. "Just one more migration" keeps showing up in standup

If you've been "almost done" with the same migration for two quarters, you're not actually almost done. You're running a parallel codebase. Every feature now ships twice — once in the old shape, once in the new.

Concretely, you have code that looks like this scattered everywhere:

ts
function getUser(id: string) {
  // TODO(2024-Q2): remove old path after migration
  if (FEATURE_FLAGS.newUserService) {
    return userServiceV2.fetch(id);
  }
  return legacyUserModel.findById(id);
}

Every if (FEATURE_FLAGS…) is a code path you're paying to maintain *twice*. Every new field has to be added on both sides. Every bug fix has to be diagnosed against both implementations.

The honest move is one of two things:

  1. Commit to finishing. Freeze new features for two weeks, ship the migration, delete every old code path. Move on.
  2. Abandon it. Roll the flag back, delete userServiceV2, take the loss.

The half-done state is the most expensive option, and it's the one most teams pick by accident.

3. The test suite takes 20 minutes and everyone runs it locally to "warm cache"

A 20-minute test suite means engineers context-switch out of their work *every time they run it.* Most don't run it. CI catches things, but by then the engineer is two PRs deep into something else.

Healthy targets I've seen at teams that ship fast:

  • One file's tests: under 5 seconds
  • One module's tests: under 60 seconds
  • Full suite in CI: under 10 minutes
  • Pre-commit hook: under 3 seconds total

Tests should run in under 60 seconds locally for the file you're editing. If they don't, the test suite isn't a safety net — *it's a tax.* And taxes don't make people more careful; they make people avoidant.

A test you don't run is worse than no test, because it gives you the false confidence of coverage.

Two patterns that consistently work:

ts
// Pattern 1 — Co-locate tests with source
// src/billing/calculateGst.ts
// src/billing/calculateGst.test.ts   ← runs in <1s on its own

// Pattern 2 — Test pyramid, enforced
// 80% unit, 18% integration, 2% end-to-end.
// If your shape is inverted, your suite will be slow no matter what.

4. You hire seniors who get worse as they ramp up

Senior engineers come in at full speed. Three months in, they're slower than your mid-level engineers. Six months in, they're hostile in design reviews.

This isn't about culture. It's about the codebase punishing competence.

Seniors notice things mid-levels don't:

  • The same logic implemented inconsistently in OrderService, OrderRepository, and OrderManager
  • Abstractions that promise more than they deliver — a Strategy pattern with one strategy
  • "Helper" files that are 4,000 lines of accumulated edge cases
  • Names that lie: validateUser() that also saves, getOrders() that also charges cards
  • Comments that contradict the code

Your mid-level engineers learned the rules of *your specific codebase*; your seniors are trying to write code that would work in *any* codebase, and yours fights them.

When a senior engineer says "I don't know where to put this," they're not confused. They're telling you the codebase has no shape.

5. Estimates are accurate for small tasks and 4x off for big ones

If two-hour tasks ship in two hours but two-week features ship in eight weeks, you don't have an estimation problem — you have a leverage problem. The codebase doesn't compose.

The math is simple but rarely written down:

text
expected_effort  = scope × complexity
actual_effort    = scope × complexity × coupling^scope

For small scope, coupling^scope is close to 1 and your estimate holds. For large scope, that exponent compounds — and the bigger the feature, the more it touches, and the more disproportionately expensive it gets.

This is the most expensive signal on the list and the easiest one to dismiss as "well, big features are hard." They're not supposed to be *that* much harder.

What to actually do about it

The temptation is to call a meeting, pick a problem area, and start refactoring. *That's how 18-month rewrites begin.*

The better move is to instrument first. For one sprint, ask the team to track — informally, in a shared doc — the moments when they hit one of these five signals. Don't try to fix anything yet. Just collect data.

A simple template works:

yaml
date: 2026-05-29
signal: "PR review > 24h"
where: "billing/invoiceCalculator.ts"
why: "Reviewer needed to mentally simulate the GST rounding path"
estimated_cost: "~4h reviewer time"

At the end of the sprint, you'll have a ranked list of the specific code paths that are bleeding. Pick the most expensive one:

  1. Fix it
  2. Measure if the signal got quieter
  3. Move to the next

It takes longer than the rewrite would have. It works, and the rewrite wouldn't have.

Diagnose before treating — same principle in code as in medicine

Want a second opinion?

This is the kind of work we help engineering leaders do at OrbitNexa — turning vague *"the team is slow"* intuition into specific, measurable codebase fixes. If any of these signals sound familiar, book a 30-minute architecture review — we'll spend it on your codebase, not our pitch.