Hands-on · 21 days · 4 hours a day

Rebuilding coding fluency by hand

HANDS
ON

A year of letting agents write the code, three weeks to get it back by hand. This is the plan for spending those hours where they move the odds most: what to practise, in what order, on which day, and when to stop.

AI offTypeScriptBenchmark ≈ Oct 19Key-value store dojo
79h
of practice
68%
hands on keyboard
3
go / no-go checkpoints
1
bottleneck to fix first
01 · The bottleneck

Five skills. One of them is holding the rest back.

Hands-on work at this level runs on five skills, and each one needs a different kind of practice. Coding without AI is the bottleneck. It's the one that has faded most, and the rest (design, deep dives, explaining decisions) sit on top of it. That's why the first three weeks aim almost entirely at it, with the others at a lower daily dose. Phase 2 turns the dial toward design.

02 · Priorities

How much it matters × how far you are from ready

Every skill sits somewhere on this map. Up means it matters more; right means your gap is bigger. The size of each square is the hours this plan gives it. Top right gets the most hours. Bottom left can wait for Phase 2. Tap a square.

Ranked by hours in the 21 days

    Weights and gaps are judgement calls, based on your dojo ledger (Aug 12, 44 days ago) and what strong product engineering asks for day to day.

    03 · Where the hours go

    The mix shifts as the benchmark gets closer

    Week 1 rebuilds fluency. Week 2 makes your code hold up when requirements change. Week 3 is game conditions: timed sessions, talking out loud. Design and stories run alongside at a lower dose every day. A little each day sticks better than cramming it all into Phase 2.

    “Phase 2” is an estimate for the two weeks after the benchmark, when design and stories take over.

    04 · 21 days

    Every day, already decided

    Tap any day to see its four hours. Yellow squares are milestones. Mark a day done when you finish it (the mark stays in this browser). Dojo modules unlock one at a time. I write the next module after reading how the last one went, so a later day may change if the ledger says it should.

    SunMonTueWedThuFriSat
    05 · The shape of a day

    Four hours, three kinds of day

    Almost every day has the same skeleton, so you never spend energy deciding what to do next. New material comes first, while you're fresh. The cold rewrite comes after a break, because recalling from a blank page is what turns a solved challenge into something you can do again next week.

    06 · Checkpoints

    Three honest check-ins

    A plan only works if you check it against reality. On each of these days, test yourself against a pass condition that's hard to fudge. If you miss one, the plan changes. You don't just push on harder.

    07 · Rules of the camp

    Eight rules that make the hours count

    Most of the efficiency comes from not wasting reps. Each of these rules exists because the easy alternative feels productive and teaches less.

    08 · Short on time

    If you only have ten days

    Cut breadth, not the daily structure. Keep the cold rewrites and the timed sessions; they're where the gains are.

    Days 1–3

    Dojo modules 1–3: core store, expiry, scans.

    Days 4–6

    A repair day, then transactions and async persistence. Skip snapshots, nesting and watchers.

    Days 7–9

    Checkpoint, then two timed sessions with review.

    Day 10 + daily

    Taper. Design drops to 30 minutes a day (Modules 2 and 4 only), plus your summary and one deep dive.

    09 · Module 01

    Code that absorbs change

    Why this comes first

    You're about to put about fifty hours into practice. If you practise the wrong thing, like raw speed, puzzle tricks, or getting everything finished, those hours won't turn into better work. So before any drilling, get precise about what separates strong hands-on engineering from fast typing.

    Real problems grow in parts

    Requirements rarely arrive all at once. Part 1: a store that can set and get. Once it works, part 2 lands: keys should expire. Then part 3: transactions. Almost nobody's first design is perfect. What matters is what each new part costs you.

    Take part 2, expiry. Engineer A kept values in a plain object and checked Date.now() inside get, count and delete. To test expiry they need to control time, and there's no way in. So they start patching three functions under deadline. Engineer B routed every read through one internal read(key) and took a now function in the constructor. For them, part 2 is a six-line change in one place. Same intelligence, very different part 2.

    Interactive · watch the parts land

    Tangled

    Bounded

    Line counts are illustrative, but the shape is the real thing.

    What strong hands-on work looks like

    Five things, in about this order of importance:

    • It works. Code that runs, on the cases that matter, including the obvious edge cases.
    • It absorbs change. Parts 2 and 3 slot in without a rewrite. This is the big one, and it's all about where your boundaries are.
    • You narrate. You say the trade-off before you type it, so whoever you're pairing with can follow your reasoning.
    • You test and debug calmly. You run small cases, read the error, and form a hypothesis. No flailing.
    • Pace. Enough progress to reach parts 2 and 3. It's last on purpose.

    The misconception: “I need to be faster”

    Pace problems are almost never typing problems. They're rewrite problems: part 2 forces you to tear up part 1. Speed comes from not having to redo work, so the drills here train structure, not keystrokes. The second trap is cleverness. A slick one-liner that's hard to extend loses to a boring function with a good name.

    The exception: when to hack on purpose

    With a deadline minutes away, the right move is sometimes to take the shortcut. The difference is whether you say so: “Normally I'd pull this into a helper, but I'll inline it to get part 3 running and come back to it.” A trade-off you name out loud reads as judgement. The same shortcut taken silently reads as sloppiness.

    Where this leads

    This is why the plan spends so much time on API boundaries and async coordination. They're the two places where part 2 costs either five lines or a rewrite, and they're the two areas your ledger says never landed. It's also why the architecture track starts with queues next. That's the same instinct at system scale: decide where work is handed off and where state changes, then make those the only places it happens.

    Check 1 · You're 25 minutes into a timed session and realise your part-1 design won't take part 2 cleanly. Refactor or push on?

    Say it out loud, then estimate. If the refactor takes under about five minutes and unblocks parts 2 and 3, do it: that call is exactly the judgement you're practising. If it's bigger, name the problem, patch around it, and keep moving. Silently pushing on is the only wrong answer.

    Check 2 · Why is taking a now() function in the constructor an API-boundary decision, not a testing trick?

    Because time is an input. Anything your code depends on from the outside world should come in through a boundary you control. Tests just happen to be the first caller that needs a different clock. The next caller might be a replay tool or a simulation.

    10 · Module 02 · Architecture

    Why queues exist

    Why this comes first

    Almost every architecture diagram you'll ever draw has a queue in it somewhere, and most people draw it because diagrams like that usually have one. If you can't say what a queue buys and what it costs, every box after it is guesswork. So we start with the problem a queue solves, before we even use the word.

    The problem: three ways two pieces of work get chained together

    Picture a generation finishing. Afterwards you want two more things to happen: record it in the user's library, and compute an embedding so it's searchable by meaning. The obvious way is to do both inline: the request finishes, calls the library service, calls the embedder, then returns to the user.

    That chains the request to the follow-up work in three separate ways, and each one hurts on its own:

    • Time. The user waits for the follow-up work, even though they don't care about it right now.
    • Rate. A burst of 10× traffic hits the embedder at 10× immediately. It has to be sized for your worst minute, not your average one.
    • Failure. If the embedder is down, generations fail too. A search feature takes down image generation.

    The idea: put a buffer between who asks and who works

    A queue is a durable place to leave work. The producer writes a message and moves on. Consumers read messages when they have capacity. Nothing else changes. The work is the same, it just no longer happens while someone waits for it. That one move breaks all three chains: the user doesn't wait, bursts pile up in the buffer instead of hitting the consumer, and a consumer outage becomes a growing backlog instead of an error.

    Try it. Push the burst up, drop consumer capacity, or break the consumer for a stretch, and compare the two modes.

    Interactive · inline vs queued

    A toy model: steady 50 arrivals per tick, a burst from tick 15 to 25, and an optional outage from tick 20 to 35.

    What a queue gives you, and what it costs

    Gives

    Time decoupling. The producer finishes without waiting.

    Rate smoothing. Bursts get absorbed, as long as consumers keep up on average.

    Failure isolation. A consumer outage becomes a backlog, not an error.

    Independent scaling. You add consumers without touching the producer.

    Costs

    “Done” no longer means done. The request returns before the work happens, so the result has to be polled for or pushed.

    Duplicates. Most queues deliver at least once, so the same message can arrive twice. That's Module 3.

    A backlog can grow forever. A queue buys time, not capacity. That's Module 4.

    New ways to be blind. You now have to watch how old the oldest waiting message is, not just whether anything errored.

    Two shapes: a work queue and a log

    “Queue” covers two shapes, and mixing them up is behind a lot of confused diagrams.

    • Work queue (competing consumers). Each message goes to one worker. Add workers to go faster. This is right when a job should happen exactly once, like “resize this image”.
    • Log or stream (fan-out). Every message is kept in order, and each consumer group keeps its own place in it. Two different features can each read every message at their own pace. Within one group, workers still compete.
    Where you've already shipped this

    The Assets pipeline is the log shape. Every finished request is appended to a stream and the request moves on. The library-catalog work and the embedding work are two separate consumer groups, so each sees every request, and a slow embedder never holds back the catalog. Inside each group, several pods compete for entries.

    request finishes ──append──▶ stream ──┬──▶ group: catalog ──▶ library └──▶ group: embed ──▶ search index

    And the front door of generation itself is the work-queue shape: submit a job, get an ID back, then poll or get a webhook for the result. A long generation would time out a plain HTTP call. The queue turns “wait for it” into “come back for it”.

    Ask Claude for the file-level tour of your own code. It's kept off the Shelf.

    The misconception: “a queue makes the system faster”

    It doesn't. A queue moves the waiting from the user to the backlog. If consumers are slower than arrivals on average, the queue just fills, and nothing drains it. Set capacity to 45 above and watch. The user side looks perfect while the backlog climbs without end. Queues fix spikes. Only more capacity (or doing less work) fixes a deficit.

    The edge case: when lag looks like zero but you're losing work

    Streams don't keep messages forever. They trim anything older than a retention window. A consumer that falls behind by more than that window doesn't fail. Its oldest messages are just gone, and a lag metric can read fine while it happens. That's why the right alert on a stream is the age of the oldest unprocessed message, measured against the retention window, not an error count. Your Assets pipeline alerts on exactly this.

    Where this leads

    We just said most queues deliver at least once. Think about what that means for the embedder: a pod takes a message, does the work, and crashes before confirming it. The message comes back, and someone does the work again. Is that safe? It depends on how the work is written, which is Module 3: retries, duplicates, and idempotency.

    Check 1 · The embedder is down for 20 minutes. What does a user notice in the inline design, and in the queued one?

    Inline: their generations fail, or hang until a timeout, for 20 minutes. A search dependency broke the core product. Queued: generations work normally. New images just take a while to become searchable, and the backlog drains once the embedder recovers, provided the outage is shorter than the stream's retention window.

    Check 2 · Arrivals hold at 100 per second, and your consumers handle 80. Does adding a queue help?

    Only for a while. The backlog grows by 20 a second, which is 72,000 an hour, forever. The queue turns an immediate failure into a slow one. The real fixes are more consumers, cheaper work per message, or deciding which work to shed. That's backpressure, Module 4.

    Check 3 · Why does each Assets hook get its own consumer group instead of sharing one?

    Because every request has to reach both the catalog and the embedder. In a shared group, each message goes to only one consumer, so each hook would see about half the requests. Separate groups also mean each hook keeps its own position, so one slow hook can't hold the other back.

    11 · The architecture track

    Twelve modules, fundamentals first

    This doc teaches the architecture half, bottom up. Every module takes one idea (why a queue, why a retry needs a key, why a job needs checkpoints), builds it from the problem it solves, and then finds it in something you've already shipped. Each module is written when you reach it on the calendar, not before. The coding half lives in its own dojo, TypeScript, cold.