Onboarding juniors alongside coding agents

Onboarding juniors alongside coding agents

Month four, two juniors, one on-call slot. One of them had shipped more PRs than anyone else in the cohort. The other had shipped almost nothing. We gave the pager to the one with almost nothing. The reason was a number nobody on the team had been tracking six months earlier.

On paper it looked like a mistake. A year ago, if you'd shown me those two PR counts and asked who was ready to carry a pager, I'd have pointed at the bigger number without thinking. The lead engineer in that meeting pushed back hard, and he was right to. One column said "productive" and the other said "slow". We picked slow.

The explanation starts with a retry path that broke on a Tuesday.

The incident where the code was fine

The incident was in a retry wrapper around a third-party integration. A downstream service started returning slow successes instead of clean failures. Our retry logic treated the timeout as a failure and retried. The original request eventually succeeded as well, so we got duplicate writes. Not a disaster. Every team has a few of these a year.

The last PR to touch that wrapper had a junior's name on it. The agent wrote the diff, the junior opened the PR, and a senior approved it. On the call I asked the junior to walk us through what the retry did when the upstream was slow but not dead. They couldn't. They weren't dodging the question. Nothing in their four months had ever made them think about it.

The code was fine, for the record. The diff did what the ticket asked, and the bug came from an assumption older than the PR. That wasn't what bothered me. This person had shipped plenty of code and had never been anywhere near a failure.

The starter ticket was never about the code

Remember how juniors used to learn. You got a "good first issue": add a field, fix a flaky test, bump a timeout, write a small migration. Small work, but the size wasn't the point. Doing it dragged you through the parts of the system that bite. The migration locked a table you didn't know was hot. The flaky test turned out to be a race. Your timeout bump got a review comment from someone who had been paged at 3am over that exact value.

Those tickets were a subsidy. They were the cheapest way we had to show new people how our system fails, while getting it wrong still cost almost nothing.

Coding agents are very good at exactly those tickets. Over about a year, without anyone deciding it, the agents took the whole subsidy. Juniors stayed busy and kept opening PRs, but the PRs were now a wrapper around agent output. Output stopped telling us anything about growth, because output had become the cheap part.

Fix #1: reserved learning tickets (the second attempt)

The obvious fix was to keep some tickets away from the agents. Our first attempt failed, and I suspect most teams will fail the same way.

We reserved the easy ones: small UI tweaks, copy changes, simple CRUD endpoints. Start juniors where it's safe, we thought. In practice they became slower typists than the agent. They produced the diff the agent would have produced, took three times as long, and learned about as much as you learn from retyping a recipe. They knew it. One told me in a 1:1 it felt like being handed a colouring book.

The rule we landed on: **pick tickets for the failure they expose, not for how easy they are.** Agents keep the trivial stuff. Juniors get a migration on a table with real traffic, a flaky integration test, an idempotency gap, a retry seam like the one that broke on us. The ticket can be small. It has to sit somewhere our system has failed before, or probably will.

Day to day, when a ticket lands in the backlog the tech lead asks one question: "What does this teach someone about how we break?" If the honest answer is "nothing", the agent does it. If it's "how our queue behaves when a consumer dies halfway through", it gets a learning label and goes to a junior. The junior can use an agent on it. They can't merge it until they can explain the failure mode to a senior.

Fix #2: shadow the agent

Reserved tickets covered maybe a fifth of a junior's week. The rest of the time they were still working beside agents, and that time had to teach them something too.

The ritual is simple. Before you open the agent's diff, write down what you expect it to be. Not code, just a few lines in the ticket: which files, what approach, which edge cases it ought to handle. Then open the diff and compare.

One example stuck with me. The ticket: add a nullable column to an orders table and backfill it. The junior predicted "Migration adds the column, separate script backfills in batches, model gets a default." The agent's diff added the column and ran the backfill inside the same migration as a single UPDATE.

Each gap between prediction and diff lands in one of three buckets:

  • **The junior missed a constraint.** The agent knew something they didn't, like a helper we already have or a convention in that module. Cheap lesson. Write it down and move on.
  • **The agent took a shortcut.** The prediction was right and the diff cut a corner. The orders backfill went here: one UPDATE on a hot table inside a migration is a lock waiting to happen, and the junior's prediction caught it.
  • **Both were wrong.** Prediction and diff disagree, and when a senior looks, neither holds up. This bucket matters most.

The third bucket paid for everything

A few weeks into shadowing, a junior predicted a diff for a webhook handler and got it wrong. They assumed we deduplicated events by provider event ID. The agent's diff didn't deduplicate at all. They took it to a senior to ask which of them had misunderstood. It turned out the junior had the right instinct for the wrong reason: we didn't deduplicate anywhere on that path, and the provider retries webhooks. A senior had already approved the agent's diff.

Nobody was careless. The senior reviewed what was on the page, and what was on the page was clean. The junior caught it because they'd built a model of the system before seeing the code, so the missing piece stood out. Read the diff first and you review what's there. Predict first and you start noticing what isn't.

That changed how the seniors felt about the whole thing. Before, onboarding was babysitting, a tax on their week. After the webhook catch, a junior's prediction was one more independent check on agent output, which is exactly what seniors are drowning in now. Onboarding started paying for itself in code quality. I hadn't planned for that. I'd have settled for juniors who could explain a retry.

Fix #3: the crossover point

Once juniors were reviewing agent diffs properly, we had something to count. We kept it simple enough for a spreadsheet, because anything fancier would have been dead within a month.

  • **Catch:** the junior flagged a real problem in an agent diff, confirmed by a senior or by the fix that followed. Style nits don't count.
  • **Miss:** a problem showed up later, in senior review, CI, staging or production, in an agent diff the junior had already reviewed and passed.
  • **Crossover:** the first week a junior's catches outnumber their misses across a rolling three-week window.
text
week | junior | diffs_reviewed | catches | misses | rolling_3wk_catches | rolling_3wk_misses | crossed?
-----+--------+----------------+---------+--------+---------------------+--------------------+---------
  5  |   A    |      ...       |   ...   |  ...   |        ...          |        ...         |   no
  6  |   A    |      ...       |   ...   |  ...   |        ...          |        ...         |   no
  7  |   A    |      ...       |   ...   |  ...   |        ...          |        ...         |   YES

Now the parts that don't make a tidy success story.

It came later than we expected. I'd assumed a few weeks of shadowing would do it. For most of the cohort it took much longer, and some still hadn't crossed by the end of the period I'm describing.

It also went backwards, sometimes for weeks. A junior would start catching more than they missed in one service, get moved to a corner of the codebase they'd never seen, and the misses would pile up again. That's why we use the rolling window: the weekly numbers swung too much to read. Even with the window, the line was bumpy. You read it as a trend, squinting a bit. It isn't a gate that opens once and stays open.

And counting misses is socially awkward. Nobody enjoys "you missed this" logged against their name. We said up front, and kept saying, that the sheet measures exposure, not performance. Misses are expected, especially early on. A junior with zero misses usually hasn't reviewed enough agent diffs to have been wrong yet.

Back to the two juniors

With the sheet on the table, the on-call call stopped looking strange.

Junior A crossed over in week 7. They'd shipped almost nothing because their reserved tickets were slow, awkward and sat on seams: a migration, a flaky payment integration test, the retry wrapper from the incident. The rest of their week went into shadowing, not churning out PRs. Their predictions were often wrong in the useful way. By week 7 they were catching more in agent diffs than they let through.

Junior B led the cohort in PRs and still hadn't crossed by month four. B isn't weak. B was very good at the job we'd accidentally been rewarding: take a ticket, drive the agent, get it green, open the PR. Almost all of B's misses were the same kind. They passed diffs that were correct locally and wrong at the system level, which is precisely the kind of thing that wakes you up on call.

So A went on rotation first, and mostly it worked. A's first real page was a backed-up queue. They found the stuck consumer and knew why it mattered, because one of their reserved tickets had been on that consumer.

Week two nearly went wrong. An alert fired in a reporting service none of A's reserved tickets had touched, and they froze for longer than I'd like before escalating. Going back through the sheet afterwards, every one of A's catches was in three services. The crossover was real but local. We'd read one number as general readiness when it was readiness for a specific area. Now we track crossover per service area, and a junior's first on-call shifts are scoped to the areas where they've crossed.

B has since moved onto reserved tickets and is shadowing properly. Their PR count went down, and the misses are starting to come down too. That's as much as I'll claim for now.

The planning-meeting fight

The hardest conversation wasn't with the juniors. It was in quarterly planning, when someone from leadership looked at the board and asked a fair question: "The agent could do these tickets in an afternoon. Why are we paying a junior to take a week?"

I lost that one the first time because I argued it as a learning and culture issue. Learning matters, but it's the first thing cut when a roadmap slips.

What won the second time was pulling the ticket out of the cost calculation entirely. We aren't paying a junior to close that ticket. We're paying for an engineer who can carry a pager in six months, and the ticket is the cheapest training environment we have. The agent can close the ticket. It can't hold the pager. If we don't build that capability deliberately, we pay for it anyway, later: seniors stuck on call forever, or an expensive outside hire.

Then we put it on the plan as a line item instead of a favour. Reserved learning tickets appear as an explicit apprenticeship allocation, capped at a share of each junior's sprint, with a named deliverable: the crossover sheet, reviewed monthly. Leadership was fine with a cost they could see and a result they could check. What they'd objected to was an invisible slowdown backed by a vague promise.

What to steal on Monday

If your juniors work next to coding agents, here are three things you can start this week. No tool, no budget, no reorg:

  • **A rule for picking tickets.** Before assigning one, ask "what does this teach someone about how we break?" Nothing? The agent takes it. A migration, a flaky integration or a retry seam? Label it and hand it to a junior.
  • **A review ritual.** Shadow the agent: write the predicted diff before opening the real one, then sort every gap into missed constraint, agent shortcut, or both wrong. The third bucket goes to a senior, every time.
  • **A metric.** Catches versus misses on agent diffs, rolling three-week window, tracked per service area. Watch for the crossover and expect it late and uneven. Stop using PR count as a measure of growth.

Old-style onboarding assumed juniors grow by writing code, so more code meant faster growth. That assumption broke quietly. Agents do the typing now, and they're good at it. The junior's job is judgment, and nobody gets that by default. You have to make room for it.

Discussion

Ask my AI about this article. The chat stays on your device; when you're done you can publish a short summary for other readers.

No questions yet. Try: what's the trade-off here, or how would this fail in practice?