What I Learned Shipping 300 Code Reviews in 1.5 Years as a Fresher
From fixing undefined variables to decomposing a monolith — a career progression compressed into data.
The Numbers
In 18 months as an IC-1 (fresher-level individual contributor), I authored 302 revisions on Phabricator:
- ~200 published (landed in production)
- ~80 abandoned (scrapped, restarted, superseded)
- ~20 in other states (needs review, draft)
That’s roughly 2.5 published revisions per week, sustained over 1.5 years.
But the numbers alone don’t tell the story. The type of work changed dramatically from month 1 to month 18.
Phase 1: The Bug Fix Grind (Months 1-6)
My first 100+ revisions were almost entirely bug fixes:
D27158: Fixes Undefined var TypeError: primarySkills is undefined
D27181: Fix no space between text area and rating symbols
D27293: Fixed undefined var TypeError: $element[0] is undefined
D27295: Fixes Undefined var Error: filters is undefined
D27310: Fixes Resume modal: No space after comma
D27370: Fixes Undefined var Error: action_stages is undefined
D27398: Fixed tooltip not showing for request evaluation
These look small. They are small. But here’s what they taught me:
What I Learned From Bug Fixes
-
How to read a stack trace. Sentry errors with JS undefined vars forced me to trace execution flow through AngularJS controllers and services.
-
How the codebase is structured. Every bug fix required understanding which controller owns which scope variable, which service fetches which data, which template renders which state.
-
How to write a good revision summary. Early on, my summaries were “Fix bug.” By month 3, they included: what the bug was, what caused it, what I changed, and how to test it.
-
How to handle the review process. My first revisions had 3-4 rounds of review feedback. By month 4, most landed in 1-2 rounds.
-
Pattern recognition. After fixing 20 undefined variable bugs, I started seeing the anti-patterns that produced them. This became useful later when I started writing features.
The Uncomfortable Truth
Bug fixes are not glamorous. Nobody’s impressed by “Fixed undefined var.” But they’re the fastest way to build:
- Codebase familiarity
- Trust from the team
- Confidence to make larger changes
Phase 2: Small Features and Polish (Months 6-10)
Around month 6, my revisions shifted:
D29044: Fix cancel evaluation modal width and height issue
D29884: Fix evaluation bugs for interviewer role
D30267: Fix evaluation tab count issue changing issue on applying filters
D30401: Add graceful handling for profile type mismatch
D30483: Simplify evaluation count logic in templates
D31016: Return Credit Info and Failure Message on Bulk Action Errors
The last one — Return Credit Info and Failure Message on Bulk Action Errors — was my first “feature” revision. Not a bug fix. Not a UI tweak. A deliberate backend enhancement that required designing a response format and coordinating with the frontend.
What Changed
- I started proposing solutions instead of being assigned bugs
- I began touching the Python/Django layer, not just JS/CSS
- I started owning small end-to-end flows (API → frontend → user message)
Phase 3: Architecture and Performance (Months 10-18)
This is where the work got interesting:
D28249: Separate Python Code for Evaluations
D28269: Separated CSS Code For Evaluations
D28365: Separate JS Code For Evaluations
D28367: Separate HTML Code for Evaluations
D28427: merge code-separation into master
D34678: Add Custom Browser Find Feature
D35117: Optimize Queries in get_candidate_stages API
D36701: Log function arguments for celery tasks
D36704: Restrict registering app APIs into base v1 API
D37393: Improve performance of GET /candidate/{identifier}
I was now:
- Decomposing the monolith — multi-revision architectural changes
- Optimizing queries — reading EXPLAIN plans, adding select_related
- Building features from scratch — the custom Ctrl+F search
- Writing infrastructure tooling — lint rules, logging
- Documenting methodology — Phriction docs for the team
The Abandoned Revisions
80 out of 302 revisions were abandoned. Early on, this felt like failure. Now I see it differently.
Common reasons for abandonment:
1. Wrong approach discovered during review
D37385: Improve performance of GET /candidate/{identifier} - API Group
(Abandoned → superseded by D37393 with a better approach)
I’d write a solution, get review feedback that a different approach was better, and start fresh instead of frankensteining the original.
2. Exploratory revisions
D35740: Logging for T43722
D35011: Logging for T42059
These were diagnostic revisions — add logging, observe production behavior, remove logging. The logging itself was never meant to ship permanently.
3. Scope too large, needed to split
D34541: Move all template logic to JS Code for Easier Debugging
(Abandoned → broken into smaller focused revisions)
4. Genuine mistakes
D36712: Test linter added in D36704
(Oops, committed a test revision)
The Lesson
Abandoned revisions are cheap. Bad code in production is expensive. If your alternative to abandoning is shipping something you’re not confident in, abandon every time. The best engineers I work with have high abandon rates because they iterate aggressively.
What I Wish I’d Known on Day 1
1. Read the codebase before writing code
My first few revisions had embarrassing mistakes — reimplementing things that already existed, using patterns inconsistent with the rest of the code. Spend your first week reading, not writing.
2. Small revisions > large revisions
My monolith decomposition landed as 5 separate revisions. If I’d tried to do it in 1 massive diff:
- Reviewers wouldn’t have reviewed it carefully
- A revert would have been all-or-nothing
- I couldn’t have gotten incremental feedback
Target 100-300 lines per revision. If it’s bigger, ask: “Can this be split?”
3. Write the summary as if the reviewer knows nothing
# Bad summary
Fix the bug
# Good summary
Fix: Evaluation count shows wrong value on search page after adding note
Bug: After adding a note from the search page, the evaluation count
badge increments by 1 even though no evaluation was submitted.
Cause: The note-creation event triggers the same event listener that
evaluation submission uses. The listener doesn't distinguish between
event types.
Fix: Add event type check in the listener. Only increment count when
the event type is 'evaluation_submitted'.
Test: Add note on search page → count stays the same.
The second version reviews itself. The reviewer can verify the fix just by reading the summary.
4. Understand the system, not just the symptom
My best work came from asking “why does this pattern exist?” rather than “how do I fix this instance?”
- Seeing repeated undefined var bugs → led me to understand AngularJS scope inheritance
- Seeing repeated CSS conflicts → led me to propose app-scoped CSS
- Seeing repeated API namespace collisions → led me to write the lint rule
5. Document what you learn
I wrote two internal docs:
- “How to Run Compress Locally” — because I spent 2 hours figuring it out and didn’t want anyone else to
- “How to Separate an App” — because the monolith decomposition process wasn’t written down anywhere
These took 30 minutes each. They saved hours for every new hire after me.
The Progression, Visualized
Month 1-3: ████████████████████████ Bug fixes (undefined vars, CSS)
Month 4-6: ████████████░░░░░░░░░░░░ Bug fixes + UI polish
Month 7-9: ████████░░░░░░░░████████ Features + App separation
Month 10-12: ████░░░░░░░░████████████ Performance + Architecture
Month 13-18: ██░░░░████████████████░░ Architecture + Infra + Features
The ratio of “fix someone else’s bug” to “build something new” steadily inverted.
Metrics That Actually Matter
Looking back, these are the metrics that correlated with real growth:
| Metric | Why It Matters |
|---|---|
| Revisions per week | Consistency > bursts. 2-3/week means you’re reliably shipping. |
| Lines of code per revision | Smaller = better reviewed = higher quality. |
| Review rounds before landing | Decreasing over time means you’re internalizing the codebase standards. |
| Scope of changes | Are you touching 1 file or 10? Single-layer or full-stack? |
| Who assigns your work | Self-assigned > assigned. It means you’re spotting problems. |
| Abandoned-to-published ratio | A healthy ratio (~20-30%) means you’re experimenting, not just playing safe. |
Advice for Other Freshers
-
The bug fix phase is an investment, not a punishment. You’re building a mental model of the system. This pays off exponentially when you start building features.
-
Ship frequently, not perfectly. A revision that ships with one round of feedback is better than a “perfect” revision that takes a week.
-
Go beyond your ticket. If you fix a bug and notice two more, fix those too. If you see a pattern, propose a systemic fix. This is how you get trusted with architecture work.
-
Read other people’s revisions. I learned more from reviewing my teammates’ code than from writing my own. You see patterns, approaches, and mistakes you can avoid.
-
Own a module. After 6 months, I became “the evaluations person.” This wasn’t assigned — I just kept fixing evaluations bugs until I understood it better than anyone. Then the decomposition work was a natural next step.
-
Write things down. Two 30-minute docs earned me more visibility than 50 bug fixes. Documentation signals that you’re thinking about the team, not just your own output.
Where It Led
After 18 months:
- Owned the full evaluations module end-to-end
- Optimized 4 high-traffic API endpoints
- Built architectural tooling (lint rules, logging)
- Documented repeatable engineering processes
- Contributed to the Python 3 migration
From a career positioning standpoint, this body of work is equivalent to what many engineers accumulate in 3-4 years — because the volume was high, the scope expanded continuously, and I documented everything.
The 302 revisions aren’t the point. The arc from “fix undefined variable” to “decompose the monolith” — that’s the story.