Skip to content

Incremental Development

Large migrations fail when teams batch too many changes together. A 2,000-line commit that refactors three services, updates the database schema, and changes the API surface is nearly impossible to review, difficult to roll back, and dangerous to deploy. If something breaks, the blast radius is the entire batch.

Incremental development is the antidote. It is not a velocity technique — it is a risk management discipline.

Every productive modernization session follows the same cycle:

1. Make one small change (one logical unit)
2. Verify: tests pass, build succeeds, lint clean
3. Commit with a clear message
4. Repeat

This rhythm applies whether you are working alone, in a team, or with AI coding agents. The cycle length is minutes, not hours. If more than 30 minutes pass without a commit, the batch is too large.

Modernization amplifies the cost of mistakes:

Risk FactorIn GreenfieldDuring Modernization
Broken buildBlocks new featuresBlocks the running production system
Failed testFeature does not workParity with legacy is broken
Bad deployRoll back to previous versionRoll back may require database migration reversal
Unclear commitAnnoying to traceCritical context lost for debugging legacy interactions

Small changes reduce all four risks. Each commit is a safe rollback point. Each verified step proves that the system still works.

Verification is not optional and not “when convenient.” It happens after every code modification, before reporting progress.

CheckWhenWhat It Catches
TestsAfter every code changeRegressions, broken parity, logic errors
BuildAfter every code changeImport errors, type mismatches, missing dependencies
LintBefore commitStyle drift, unused imports, potential bugs
Type checkBefore commitType errors that tests might miss
1. Edit payment.service.ts (extract method)
2. Run tests → 42 passed
3. Run build → success
4. Commit: "refactor: extract validatePaymentInput from processPayment"
5. Edit payment.service.ts (add new validation rule)
6. Run tests → 41 passed, 1 FAILED
7. Fix the failing test
8. Run tests → 42 passed
9. Commit: "feat: add currency format validation to payment input"

When verification fails, the response is immediate — not deferred:

Fix Immediately

A failing test after a code change means the change introduced a regression. Fix it now, while the context is fresh. The cost of fixing goes up exponentially with time.

Do Not Accumulate

Two failing tests are harder to debug than one. Five are nearly impossible. Never proceed to the next change while tests are red.

Do Not Skip

“I will fix the tests later” is how modernization projects accumulate months of hidden debt. There is no “later” — the next change depends on the current one being verified.

IdealAcceptableToo Large
Under 200 lines per commit200-500 lines per commitOver 500 lines per commit

Each commit should have a single, clear purpose and be independently deployable.

Good Commit ScopeBad Commit Scope
Extract one methodRefactor entire module
Add one validation ruleAdd validation + change API + update schema
Fix one bug with regression testFix three bugs in one pass
Add one database migrationMigration + service changes + route changes
Rename a concept consistentlyRename + restructure + add features

If a commit message needs “and” to describe what it does, it should probably be two commits.

Long-lived branches are particularly dangerous during modernization because the codebase is actively changing. A feature branch that lives for two weeks will diverge significantly from main, and the merge will be painful.

PracticeGuideline
Feature branchesMax 2-3 days before merging
Direct commitsAcceptable for small, safe changes
Release branchesAvoid — use feature flags instead
Develop branchAvoid — adds merge overhead without benefit
Main branchAlways deployable, always green

For migrations that take more than a few days, use feature flags instead of long-lived branches:

// Feature flag controls which implementation runs
if (featureFlags.isEnabled('modern-invoice-renderer')) {
return modernRenderer.generate(invoice);
} else {
return legacyRenderer.generate(invoice);
}

This approach keeps all code on main, enables gradual rollout, and makes rollback instant (flip the flag). It also allows parity testing in production with real traffic.

AI coding agents follow the same incremental rhythm. When an agent makes changes, verification should happen before reporting results:

Agent receives task
|
v
Make change -> Run tests -> Tests pass?
|
Yes: Commit, report success
No: Fix, re-run, then report

Structure agent instructions to enforce this cycle. Include test commands in your project’s AGENTS.md so agents know how to verify their work. See Structuring Context for AI Agents for details.

MetricHealthyWarning
Average commit sizeUnder 200 LOCOver 500 LOC
Commits per day (per developer)3-8Under 2 or over 15
Time between commits15-60 minutesOver 2 hours
Revert frequencyUnder 5%Over 10%
Main branch red durationUnder 15 minutesOver 1 hour

These metrics are indicators, not targets. Gaming them (splitting a logical change into 10 meaningless commits) defeats the purpose. The goal is a sustainable rhythm of verified, meaningful progress.

Every commit should be production-ready. No exceptions. This discipline is especially important during modernization, where a single broken migration can cascade through the system.

The question is not “can I ship this?” but “if this were deployed right now, would the system work correctly?” If the answer is not a confident yes, the commit is not ready.