I don't have teammates on this project. No one is waiting on my PRs, no one is blocked by my branch, and technically I could just push straight to main and call it a day. For about eight months, that's exactly what I did. Then I started opening pull requests against myself, refusing to merge them until they passed a checklist, and my bug count dropped hard enough that I'm never going back.
This isn't a productivity larp. It's a direct response to something I kept doing on a Node.js backend for a side project that grew into something people actually pay for: writing 1,200-line PRs that touched routing, database schema, auth middleware, and a new queue system all at once, then merging them at 1am because "it works locally."
🔧 The Problem
Here's an actual PR title from my own history, from back when I didn't bother with PRs at all, just commits:
commit 4a9f2c1
Author: me
add subscription billing, refactor user model, switch to bullmq, fix cors bug
One commit. Four unrelated concerns. When something broke in production three weeks later — turned out the user model refactor silently changed how email uniqueness was enforced — I had no way to bisect it cleanly. git bisect pointed at a commit that also happened to introduce a queue system, so I spent an hour reading unrelated BullMQ code before I found the actual bug in a Mongoose schema change two files away.
The mega-PR problem people are complaining about on GitHub right now — the 4,000-line diff nobody can meaningfully review — isn't really a GitHub problem. It's a batching problem. Solo devs get it too, we just don't call it a "review bottleneck" because there's no reviewer to bottleneck. The cost shows up later, as debugging tax instead of review tax.
🧩 What Changed
I started treating my own future self as the reviewer. Concretely, that meant three habits, in order of how much they actually helped.
1. One deployable concern per PR
Not one file. One concern. A PR can touch six files if they all serve the same change. It cannot touch six unrelated changes even if it's technically "one file."
Before:
feat: subscription billing, user model refactor, bullmq, cors fix
After, same work, split into four PRs merged over two days:
fix: cors origin whitelist for staging subdomain
refactor: normalize email field before uniqueness check
feat: add BullMQ queue for email jobs (behind flag)
feat: enable Stripe subscription billing on user model
Each of those is independently revertible. When the queue system had a memory leak two weeks later, git revert on one commit fixed it without touching billing.
2. Feature flags instead of long-lived branches
The old instinct was to keep a branch alive for a week while I built something big, then merge it all at once — the exact mega-PR pattern. Now I merge small, working pieces behind a flag, even when the feature isn't done.
javascript
// config/flags.js
const flags = {
QUEUE_EMAIL_JOBS: process.env.FLAG_QUEUE_EMAIL_JOBS === 'true',
};
module.exports = flags;
javascript
// services/emailService.js
const { QUEUE_EMAIL_JOBS } = require('../config/flags');
async function sendWelcomeEmail(user) {
if (QUEUE_EMAIL_JOBS) {
await emailQueue.add('welcome', { userId: user.id });
} else {
await mailer.sendNow(user.email, 'welcome');
}
}
This let me merge the BullMQ integration in small pieces — queue setup, worker process, retry logic — over four separate PRs, none of which changed production behavior until I flipped FLAG_QUEUE_EMAIL_JOBS to true in one final, tiny, easy-to-review PR:
diff
- FLAG_QUEUE_EMAIL_JOBS=false
- FLAG_QUEUE_EMAIL_JOBS=true
If that broke something, the rollback was a one-line env change, not a git revert across four commits with merge conflicts.
3. A self-review checklist before I hit merge
This is the part that actually changes behavior, because it forces a pause. Mine lives in .github/pull_request_template.md and I fill it out even though I'm the only one who reads it:
markdown
Self-review checklist
- [ ] This PR does ONE thing. If I can't summarize it in one sentence, split it.
- [ ] No schema change and feature logic in the same PR.
- [ ] New code path is behind a flag if it touches billing, auth, or queues.
- [ ] I ran this against the staging DB dump, not just local seed data.
- [ ] Rollback plan: revert commit / flip flag / neither needed.
- [ ] Diff is under ~300 lines, or I have a good reason it isn't.
That last checkbox alone killed most of my mega-PRs. "Under 300 lines" isn't a magic number — it's just small enough that I can actually reread the whole diff in one sitting and notice the thing I got wrong, instead of skimming because I already know what I meant to write.
📉 Before / After, With Real Numbers
I pulled stats from my own git log across a 3-month window before and after adopting this.
| Before | After | |
|---|---|---|
| Avg lines changed per PR | 640 | 145 |
| Production incidents traced to a merge | 6 | 1 |
Time to git bisect a regression |
~45 min avg | ~8 min avg |
| PRs reverted in full | 3 | 0 (partial reverts only) |
The incident count matters most. Five of those six "before" incidents were bugs sitting quietly inside a large diff, unrelated to the actual thing I thought I was shipping. Smaller diffs didn't make me a better programmer overnight — they just made my mistakes smaller and easier to isolate.
🚦 Where I Still Cut Corners
I'm not going to pretend this is pure discipline. Genuine one-off scripts, migrations I'll run exactly once, or throwaway debug endpoints still go straight to main sometimes. The checklist is for anything touching auth, billing, data integrity, or anything a customer would notice if it broke. Applying full ceremony to a typo fix in a README would just be theater.
🙋 Your Turn
If you're a solo dev or work on a small team with light review culture — do you actually PR your own work, or is main still your review process? I'm curious whether feature flags feel like overhead to people working on smaller CRUD apps versus something like billing or queues where the blast radius of a bad merge is bigger.
Drop your workflow in the comments, especially if you've got a better checklist item than mine — I'm always looking to steal a good one.
Top comments (0)