
You get repository access and a problem statement in the same message. The repository has years of history in it: internal packages, a routing scheme someone defended in an RFC you will never read, a data layer that was three data layers, deployment conventions that predate half the team.
The instinct is to start reading. An hour later you have read a lot of code and you still cannot predict what the system will do when you change one line.
That is the trap. Reading is comfortable, it feels like progress, and it produces almost nothing you can check.
The first hour is not for understanding the application. It is for building the smallest model that makes predictions, and then finding out which of those predictions are false.
Code tells you what could happen. The running system tells you what did. Those two things disagree more often than anyone is comfortable with, and every hour you spend only on the first one is an hour of accumulating confidence with nothing testing it.
So the useful question is not "do I understand this?" It is "what do I now expect this system to do, and how fast can I find out that I am wrong?"
Caption: The loop for the first hour. The exit condition is not "I understand the codebase," it is "my predictions survived contact with production."
Before opening a file, I want to know why I am here. A blocked migration, a slow page, a deployment that behaves differently in production, a team introducing a new rendering strategy, an agent workflow that has to talk to an existing system.
Whatever it is, it draws a line around what deserves attention. Without that line, everything looks interesting, and interesting code is the most expensive thing you can read.
Performance work pulls me toward the request path, rendering, data fetching, caching, and what actually reaches the browser. A migration pulls me somewhere else entirely: routing boundaries, shared dependencies, application state, deployment assumptions, and wherever the old architecture is fused to the new one. Same repository, different half of it.
The top level of the repository is a map of organizational decisions. One application or a monorepo. Where applications live. Where shared packages live. Whether infrastructure config sits next to product code. Whether product areas have visible boundaries at all.
A monorepo with ten shared packages implies a different operating model than an app where everything lives in one project, and that difference will show up later in how expensive your change is. package.json, the workspace config, the framework config, and the build scripts tell you more about how this team works than any component will.
Ten minutes here saves you from the most common failure: correctly understanding a file that turns out to be irrelevant.
The fastest way into a web application is to follow a single real request. Pick a page connected to the problem and answer one question: what has to happen for this URL to become something a person can see?
In a Next.js app I start at the route and work inward. What layout wraps it. What data it needs and where that data is fetched. Whether it depends on cookies, headers, auth, or search params. What happens during rendering, what happens afterward in the browser, what gets cached, and what makes the whole thing dynamic.
The value is directional. Instead of hundreds of unrelated files you get a chain, and a chain is something you can hold in your head and argue with.
Modern React makes this worth establishing early, because a component tree can look uniform while running in two completely different places. In Next.js the use client directive declares the boundary between the server and client module graphs, and once a file crosses it, everything it imports and directly renders goes into the client bundle, with the useful exception of Server Components passed in as children.
So: where are the boundaries, and why are they where they are? Does one high-level client component drag half the tree into the bundle? Where are server actions used, and are there route handlers doing work that never needed a network hop? Is the app shipping data to the browser only to send it back to the server?
These boundaries decide performance, caching, security, and bundle size at once. They also fossilize history. Sometimes a large client boundary was genuinely necessary. Sometimes one component needed a hook two years ago and the boundary grew outward from there, quietly, one import at a time. You cannot tell which by looking, and it matters, so ask.
Pick one value on the screen and walk it back to its origin: database, internal API, external service, GraphQL, CMS, or some combination that nobody planned.
Then look at how it moves. Is fetching centralized or does every route invent its own queries. Is there a service layer. Are the same requests firing twice. Does the client refetch data the server already had. Where does auth get attached, where does caching happen, and when does any of it go stale.
One value, traced end to end, usually reveals both the intended architecture and the places where it stopped being true.
Then I go looking for whatever makes independent parts of the app depend on each other. Global state is the obvious one, and its existence is not a problem by itself. What is in it is the question: auth, cart, feature flags, locale, entire API responses.
The thing I actually want to know is whether a route can render on its own or whether it requires the full application shell above it. That answer sets the size of every future change. A migration that looks like one page becomes a quarter of the app the moment that page needs a provider wrapping everything.
Identify the coupling before you scope the work, not after you have promised a date.
Code you understand and infrastructure you have not looked at will produce a confident, wrong picture.
What triggers a deploy. What gets built. Whether multiple apps deploy independently. Which environment variables exist. Whether there are edge functions, background jobs, cron, queues. Whether preview deployments are part of how people work. Whether production behaves like your laptop, which it does not.
This matters most exactly where debugging is hardest. Caching and rendering behavior are properties of the deployed system, and a local dev server will happily let you diagnose an infrastructure problem as an application bug for a full afternoon.
At this point I have a model. The point of the model is to be tested, and the fastest test is evidence from the running system: request logs, errors, traces, function duration, cache hit behavior, whatever the platform exposes. Next.js has built-in OpenTelemetry instrumentation if you need to add it, and Core Web Vitals if the problem is what users feel.
This is where the hour pays for itself, because this is where the model breaks. The route you assumed was expensive is fast. The database query is fine and three sequential awaits are the waterfall. Caching is configured correctly and something earlier in the request already made the route dynamic. That client fetch runs twice.
| Question | Where the answer actually is | What you get by reading only code |
|---|---|---|
| Is this route slow? | Traces and function duration in production | A guess based on how the code looks |
| Is this data cached? | Cache hit rate on real traffic | The cache config's intent |
| What makes this route dynamic? | The runtime's own report of the request | A list of suspects |
| Does this path fail? | Error rates and logs, by route | The try/catch blocks someone wrote |
| What ships to the browser? | The built bundle and network panel | The import graph, minus the surprises |
Every row is a place where the code is describing intent and the runtime is describing behavior. Read code to form the hypothesis. Do not use it to confirm one.
Every mature codebase has code that looks wrong out of context: an odd abstraction, a duplicate implementation, a middleware rule with no obvious purpose, a dependency that seems unnecessary, a comment that stopped making sense years ago.
The urge to clean it up is strong and worth resisting for about ten minutes. Chesterton's version of this is refusing to remove a fence until you know why it was put there. The engineering version is that the weird workaround may be the only thing preventing an incident you have never heard of, because a third-party API behaved differently in 2022, or because another team depends on behavior that is invisible from this repository.
git log -S will find when a string entered or left the codebase, which usually leads to the pull request, which usually leads to the reason. Blame, issues, and asking someone who was there cover the rest.
I do not assume strange code is good. I just do not assume I am the first person to notice it.
This is a method for getting productive fast on a bounded problem in a system someone else owns. It is not a method for owning a system.
If you are going to maintain this codebase for the next two years, read it properly. If the change is security-sensitive, or touches money, or is a one-way data migration, the runtime cannot save you from a bad prediction and the cost of being wrong is not symmetric. Slow down and read.
It also assumes production tells you things. In a system with no observability, no useful logs, and no safe way to test a hypothesis, the loop stalls at the check step. Then the first honest piece of work is adding enough instrumentation to have the argument at all, which is real work and worth scoping as such.
Once the model holds, I want the smallest change that tests it. Not a refactor. Not an architecture. Something that produces feedback: instrumentation, one data-fetching path moved, a boundary shifted, a bug reproduced in isolation, one representative route migrated.
If it behaves the way I predicted, the model got stronger and I can push its edges outward. If it did not, I learned that before proposing something large, which is the entire point. The goal of the first change is not better code. It is proof that I can predict what this system does.
By the end of the hour I want to be able to answer these without opening a file:
Every one of those is a prediction. If I can state it, I can be wrong about it out loud, and being wrong out loud in the first hour is much cheaper than being wrong quietly in week three.
There is no moment where you suddenly understand the codebase. You build a model, it breaks somewhere specific, you repair that part, and you go again. In a system large enough to be worth being careful about, that loop is the only thing on offer.
Which brings it back to the person an hour in, having read a great deal and unable to say what will happen when they change one line. They did not fail to understand the codebase. They spent the hour on the wrong deliverable. The hour does not end with understanding. It ends with a map that has a known edge, and a problem that finally sits inside it.
Thanks for reading.
More writing