top of page

Working With Code You Didn't Write

  • ShiftQuality Contributor
  • Oct 8, 2025
  • 5 min read

You've been assigned to a codebase you've never seen. Maybe you're new to the team. Maybe you inherited a project from someone who left. Maybe you're fixing a bug in a service your team doesn't usually touch. The code has no documentation — or documentation that's three years out of date. The original authors are gone. Nobody can explain why things are the way they are.

Welcome to professional software development. This is the normal state of things. The majority of your career will be spent working with code you didn't write, don't fully understand, and can't rewrite from scratch. The skill isn't writing clean code from nothing — it's navigating existing code effectively.

The Orientation Phase

Don't Read Everything

The instinct is to read the entire codebase from top to bottom. Resist it. A codebase is not a novel. It's a reference manual. You look up what you need when you need it.

Start by understanding the shape:

# How big is this codebase?
find . -name "*.py" | wc -l    # or *.js, *.cs, etc.

# What's the folder structure?
tree -L 2 -d                    # Two levels of directories

# What are the entry points?
# Look for: main.py, index.js, Program.cs, app.py, server.ts

The folder structure tells you how the code is organized. The entry point tells you where execution starts. From there, you follow the flow.

Run It First

Before you read a single line of code, get the application running. This gives you:

  • Proof that the code works (or doesn't)

  • A mental model of what the application does from the user's perspective

  • The ability to make a change and see the effect

  • Something to debug against when you make mistakes

Check the README for setup instructions. If there's no README, check for a Dockerfile, docker-compose.yml, Makefile, or package.json scripts section. If none of those exist, look at the CI/CD configuration — it shows how the system builds and runs in automation.

Find the Tests

Tests are the best documentation of behavior. They show:

  • What the code is supposed to do (the test name)

  • How to call functions and methods (the test setup)

  • What the expected outputs are (the assertions)

  • Which edge cases were considered important

# Find test files
find . -name "*test*" -o -name "*spec*" | head -20

Read the test names first. test_user_cannot_order_without_payment_method tells you more about business logic than any code comment.

Read the Git History

# Recent commits — what's been changing?
git log --oneline -20

# Who's been working on this?
git shortlog -sn

# Why was this file changed?
git log --oneline path/to/confusing/file.py

The git history tells you which parts are actively maintained (recent commits), which parts are stable (unchanged for months), and what problems have been addressed (commit messages). A file with 50 commits in the last month is a file with active issues. A file untouched for two years is either stable or abandoned.

Reading Strategies

Follow a Request

Pick one feature. Trace the path a request takes through the system:

  1. Where does the request enter? (Route handler, API endpoint)

  2. What validation happens?

  3. What business logic runs?

  4. What data is read or written?

  5. What response is returned?

Following one complete request teaches you the application's architecture — the layers, the patterns, the data flow. Every subsequent feature will follow a similar path.

Read the Types

In typed languages (TypeScript, C#, Java, Rust), the type definitions are a map of the domain. Find the main data models:

interface User { ... }
interface Order { ... }
interface Product { ... }

These tell you what the system cares about — its core domain — without reading any logic. The properties tell you what data matters. The relationships tell you how things connect.

In dynamic languages (Python, JavaScript, Ruby), look for class definitions, dataclass decorators, or schema definitions (Pydantic models, Mongoose schemas, Django models) that serve the same purpose.

Identify the Patterns

Every codebase uses patterns, even if they're not named. After reading a few files, you'll notice repetition:

  • Every API endpoint follows the same structure

  • Every database query goes through the same layer

  • Every error is handled the same way

  • Every new feature has the same folder structure

Once you identify the patterns, you can navigate new code by pattern recognition rather than line-by-line reading. "This is another endpoint that follows the standard pattern" lets you skim rather than study.

Making Changes Safely

Change the Minimum

When you modify code you don't fully understand, change as little as possible. The less you change, the less you can break, and the easier it is to identify the cause if something goes wrong.

The instinct to refactor ("this code is messy, let me clean it up while I'm here") is dangerous in unfamiliar code. The code might be messy for a reason you don't understand yet. The "unnecessary" null check might be there because of a bug that took three days to diagnose. The "redundant" variable might exist for a subtle timing reason.

Understand before you refactor. Fix the bug or add the feature. Come back to refactor later, when you understand the context.

Write a Test First

Before changing code you don't understand, write a test that captures the current behavior. If the test passes with the existing code, you know the test accurately describes what the code does. Now make your change. If the test still passes, your change didn't break existing behavior. If it fails, you broke something — and you know exactly what, because the test told you.

This is the "characterization test" approach, and it's the safest way to modify unfamiliar code.

Make Small, Reversible Changes

Each change should be a single commit that does one thing. Not a sweeping refactor that touches 40 files. One change, tested, committed. Then the next. If a change causes problems, reverting one small commit is trivial. Reverting a massive change is a project.

When the Code Makes No Sense

Sometimes the code genuinely doesn't make sense. A function that's 500 lines long. A class with 40 methods. Logic that seems to contradict itself. Before concluding "this is terrible code," consider:

Historical context. The code might have been written under time pressure, with different requirements, by a team with different constraints. Code that seems irrational often had rational reasons at the time.

Hidden requirements. The "unnecessary" complexity might handle edge cases you don't know about. The "redundant" check might prevent a race condition that only manifests under load.

Incremental evolution. A function that started as 20 lines and grew to 500 lines didn't start that way. Each addition was probably reasonable. The accumulation is the problem, not any individual change.

Understanding why code is the way it is — even if you plan to change it — prevents you from introducing the same problems that the original code was (perhaps poorly) trying to solve.

Key Takeaway

Most of your career is working with code you didn't write. Approach unfamiliar codebases by understanding the structure (folders, entry points), running the code, reading the tests, and checking the git history. Follow a single request through the system to learn the architecture. Identify patterns to speed up navigation. When making changes, change the minimum, write characterization tests, and make small reversible commits. Understand before you refactor — messy code often has reasons you haven't discovered yet.

Comments


bottom of page