I started my first internship a few weeks ago and was assigned what sounded like a small bug fix. The problem is that the project contains roughly 80,000 lines spread across a huge number of files. Most tutorials taught me to build programs from scratch, but I never practiced entering an existing codebase, tracing its execution, understanding why things were designed a certain way, or judging whether a change might affect something undocumented elsewhere. I spent my first two days repeatedly using "go to definition" and following calls through the project, and I didn't write new code until the third day. It feels like much of real development involves reading, debugging, and building a mental map of existing systems. Are there books, exercises, or practical projects that specifically help develop the skill of navigating large unfamiliar codebases?
5 Answers
What you’re experiencing is normal, and spending two days tracing definitions is not a failure. Internships and early tickets are partly about learning the architecture. Ask a senior teammate for a high-level walkthrough, identify the main layers and entry points, and take notes or draw diagrams as you go. AI tools can help summarize files or suggest areas to inspect if your workplace permits them, but treat their explanations as hypotheses and verify everything by running the code and reading the tests.
For practice, choose a small, active open-source project with documented issues and try fixing items labeled for new contributors. Read its setup and contribution instructions, run the tests, trace one request from the interface through the business logic and back, and submit a small change. Books such as “Working Effectively with Legacy Code” and “Code Reading: The Open Source Perspective” are also useful. You’ll improve faster by solving real issues than by merely browsing a repository.
Version control is another major part of code archaeology. Look at the history of the relevant file, inspect commits that introduced the behavior, and use blame or a graphical history viewer to understand context. If a feature recently broke, comparing the last working revision with the first broken one can reduce the search dramatically. Also look for an older implementation of something similar when adding a new change.
Don’t try to understand the entire repository before touching the ticket. Start with the bug: reproduce it, read the error or reported behavior, and identify the entry point. From there, follow only the execution path involved. Search for relevant names and messages, use your IDE’s call hierarchy, and keep a small map of the files and functions that matter. Most of the codebase is irrelevant to any one bug.
Starting from the failing behavior helped me too. Once you know which path actually runs, the project feels much smaller.
Learn the debugger as thoroughly as you can. Set breakpoints, step through the code, inspect variables, and compare what the program actually does with what you expect it to do. Logs and temporary diagnostic output are useful when a debugger is difficult to attach. A good workflow is to write a regression test or clear reproduction first, locate where the behavior diverges, fix it, and keep the test so the bug doesn’t return.
Sometimes deliberately changing an input or breaking a suspected dependency can make the behavior more obvious, but do that in a safe local environment. The goal is to narrow down the responsible layer, not randomly damage the project.

One warning: don’t assume unfamiliar or ugly code is automatically bad. It may encode old requirements, compatibility constraints, or years of fixes. Understand the reason and tests first, then make the smallest safe change.