The thousand token rule is free for a limited time. Available also in epub and pdf formats.
← Details

The thousand token rule

This course is fully available for free. Sign in to access all chapters.

Different entry points

The workflow so far assumes you start with requirements. But most work doesn't start there. You get a bug report at 3pm saying users can't log in. You inherit a codebase and need to understand how authentication works before you can add features. Someone asks you to modify a system you didn't build. Each of these needs a different planning approach, but the economics stay the same: figure things out in documents and tests before you start changing code.

Debugging: working backward from symptoms

A bug report gives you symptoms, not causes. "Users can't log in" could mean the authentication service crashed, the database is unreachable, passwords aren't hashing correctly, rate limiting is too aggressive, session tokens are malformed, or a dozen other things. You need to work backward from the symptom to the cause before you can fix anything.

Start by making the symptom precise. "Users can't log in" is too vague to work with. Does it fail for all users or some users? What does "can't log in" mean: wrong credentials rejected, correct credentials rejected, timeout, error page? Turn it into something concrete: "when user enters correct credentials on /login and clicks submit, response is 500 Internal Server Error." Now you have something reproducible.

The next step is generating hypotheses about what could cause this. You could sit and think through possibilities, or you can ask an AI to enumerate them given the error message and stack trace. You'll get a list: database connection pool exhausted, bcrypt library had a breaking change in the latest version, environment variable missing, middleware order changed, request payload malformed. Most of these will be wrong, but generating the list is cheap and forces systematic thinking instead of random guessing.

Rank the hypotheses by cost to test, not by likelihood. Database connection pool status takes 30 seconds to check by looking at metrics. Library version changes take five minutes to verify. Environment variables are visible in deployment configs. Test the cheap ones first. If database metrics show all connections available, you've eliminated that hypothesis in 30 seconds. Keep an investigation log documenting what you tested and what you found, because without it you'll check the same thing three times and forget you already ruled it out.

When you find the root cause, stop. Don't fix it yet. Write a test that reproduces the bug. If the problem is that bcrypt cost factor is set too high and causes timeouts, write a test that attempts authentication with current settings and expects it to complete within the timeout threshold. Run the test. Watch it fail, reproducing the bug in your local environment. Now fix the cost factor. Run the test again. Watch it pass.

This feels backward at first. You know what's wrong, why not just fix it? Because the test is your specification for what "fixed" means, and it's your insurance against the bug coming back. Without it, you're guessing that your fix works. With it, you've proved it works, and if someone changes the cost factor again six months from now, the test catches it. The test takes maybe 15 minutes to write. Fixing blindly and hoping it works means you're iterating on code at $0.078 per pass with no verification, and if the bug makes it back to production you're at the 100x multiplier.

Test before fix

If you can't write a test that demonstrates the problem, you don't understand it well enough to fix it correctly. The test becomes your specification and prevents the bug from returning silently weeks later.

Investigation: learning what exists

Sometimes the task isn't fixing or building anything, just understanding code well enough to work with it later. "How does authentication work?" seems like a straightforward question until you realize the authentication system handles OAuth, password auth, session management, token refresh, password reset, two-factor authentication, and role-based access control. You could spend a week tracing through all of that and still not have what you need.

Scope the question before you start. What do you actually need to know? If you're adding a feature that needs to validate tokens, you don't need to understand the password reset flow. Narrow it to "how does the system validate a JWT token on authenticated requests?" Now you have a specific entry point and a clear boundary.

Set a depth limit or you'll end up reading the entire codebase. Decide ahead of time that you'll trace the validation logic and its immediate dependencies, but you won't recurse into general-purpose utilities unless they're critical to understanding. The limit keeps you focused on what matters for your actual goal.

Figure out what you're producing from this investigation. Are you building a mental model for yourself so you can modify the code next week? Writing documentation for teammates? Creating a diagram of component relationships? The deliverable shapes what you extract. If it's a mental model, you need the big picture and the key decision points. If it's a diagram, you need to understand how components connect and what data flows between them.

AI works well as a reading companion here. Load the relevant code and ask it to explain what a function does, then verify its explanation by reading the code yourself. The AI might be wrong about subtle details or clever tricks, but starting with its explanation is faster than reading cold. You're checking a hypothesis instead of building understanding from scratch.

Document what you learn as you go, but keep it minimal. "Token validation happens in middleware, not route handlers. Expiration checked before signature. Custom claims validated after signature verification." These notes cost nothing to write and prevent you from re-reading the same code when you come back to it three weeks later. Eventually these notes either turn into comments, architecture documentation, or commit messages, or they get deleted once you've internalized the knowledge.

Modification: understanding before changing

Changing existing code is different from writing new code because someone already made design decisions, and those decisions might be documented, might be tribal knowledge, or might be completely lost to time. If you change things without understanding why they exist the way they do, you break subtle invariants that weren't obvious from reading the code.

Start with archaeology. Use git blame to find when each piece was added and read the commit messages. Good commit messages explain why choices were made. Bad ones just restate what the code does, but even that tells you when decisions happened, and you can look at the surrounding context. The goal is to understand not just what the code does but why it's structured that way.

The "why" matters more than the "what" when you're modifying. You can see that token expiration is set to 24 hours. The question is why 24 hours. Was it a deliberate security decision balancing convenience and risk? A UX tradeoff someone thought carefully about? An arbitrary number someone picked because they had to pick something? If it was deliberate and documented, changing it might violate requirements. If it was arbitrary, you're free to adjust based on new information.

Next, map what depends on the thing you're changing. If you're modifying token validation, what code calls it? The web frontend, the mobile app, third-party integrations, the admin dashboard, the analytics pipeline? Each dependency is a point where your change could break something. List them explicitly: "Changing token structure affects web frontend (15 components using token data), mobile app (8 API calls that include tokens), admin dashboard (token decode logic), analytics pipeline (token parsing for user tracking)." This isn't detailed implementation planning yet, just scoping the blast radius.

Assess the risk by combining blast radius with change complexity. Small blast radius and simple change means low risk, ship it. Large blast radius and complex change means high risk, maybe you need a different approach. Maybe the change isn't worth the cost. Maybe you need a migration strategy where old and new systems run in parallel for a while. This analysis happens before you've written any code, when backing out costs nothing.

Find the minimum change that achieves your goal. Adding OAuth support might not mean replacing the existing password authentication system. It might mean adding a new authentication path that runs in parallel. Smaller changes are easier to test, easier to explain, easier to roll back if something goes wrong, and less likely to break things that were working.

Once you understand what exists, why it exists, and what depends on it, you transition back to normal planning. You have requirements: "add OAuth without breaking existing password authentication." You have context: the existing system's design, its constraints, and its dependencies. Now you can follow the standard workflow: technical planning, design, test planning, implementation.

The convergent pattern

These three variants look different on the surface, but they converge on the same critical practice: write tests before you change code. For debugging, that's a test that reproduces the bug before you fix it. For investigation that leads to changes, it's tests that verify your understanding of how things work before you modify them. For modification work, it's tests that verify existing behavior still works, then tests for the new behavior.

Tests are small. One hundred to 300 tokens. You can refine them with AI assistance for $0.004 per iteration. Compare that to making code changes and discovering they don't work: you've spent $0.078 per iteration plus the time to run your full test suite plus the cognitive overhead of debugging why your fix didn't actually fix the problem. The economic advantage of getting tests right before touching code is the same advantage that makes writing requirements before implementation worthwhile. Do the cheap work first, lock in your understanding, then write code against that locked-in understanding.