Prep

Navigating and working with code written by others

What is Legacy Code?

Learning Objectives

Welcome to the Legacy Code module. In this module, you’ll be working with existing code rather than starting from scratch. This is a crucial skill for professional developers, as most of your career will involve understanding, maintaining, and extending codebases created by others.

You will need:

  • 📓 Notebook and pen
  • 💻 VSCode and Chrome Devtools
  • 🧩 The PurpleForest application (our Bluesky type app)
  • 🔍 Your analytical thinking skills, so get those Logic module notes out

What is Legacy Code?

Legacy code is any existing codebase that you didn’t write. (Or maybe you wrote a long time ago and have forgotten.) It will also likely have limited documentation, bugs, and unusual design decisions. You might have forgotten why it’s like this, but it still needs to be maintained and improved.

Sound challenging? It is! But it’s also where many developers spend most of their working lives. The skills you’ll develop in this module will prepare you for the reality of professional software development.

What You’ll Learn

By the end of this module, you will have had the opportunity to:

  • Navigate and understand an unfamiliar codebase
  • Debug existing features using systematic approaches
  • Fix issues without breaking functionality
  • Extend legacy applications with new features
  • Work effectively within existing architectural constraints

In this module you will start building the mental models you’ll need to become effective at working with legacy code. You will use a lot of models and skills you already have to help you. And let’s also talk a bit about handling the fear of breaking things. We will use logic to conquer fear.

Understanding Legacy Code

Learning Objectives

Legacy code is any code you inherit

What Makes Code “Legacy”?

Legacy code isn’t necessarily bad or even that old. It’s code that:

  1. You didn’t write - It lacks your mental model and intentions.
  2. Powers important systems - It can’t simply be deleted or replaced.
  3. Contains institutional knowledge - Sometime undocumented! Decisions were often made for good reasons which we may not remember any more, and changing those decisions may be risky.

In this module, you’ll be working with an application called Purple Forest - an basic application much like Twitter or BlueSky.

✍️exercise

Clone Purple Forest onto your computer now.

The Purple Forest application is now your legacy code. Someone else designed and built it, and you need to maintain and extend it.

Working with Purple Forest

Purple Forest has:

  • Established architecture - It follows defined patterns
  • Functional system - It mostly works, even if you don’t understand how
  • Bugs - Not everything works!
  • Multiple components - Changes might have unexpected side effects
  • Documented design - But the documentation might be incomplete or out of date

So how do you approach understanding and working with this legacy codebase?

Practices to remember

Learning Objectives

Some of our standard development practices are even more important when working with legacy code. These are things you should already be doing, but you should remember.

Formatting

We should be consistent with our formatting. We may not have any control over what decisions were made in the past. When we’re writing brand new code in a new code, we can choose whether we use two spaces or four. But when we jump into an existing codebase, we should use whatever style it already has. If we send a PR fixing a small bug, and it changes every line in the file, it’s really hard to see what changed, and what stayed the same. If we have two people joining a codebase, and one is reformatting every file they touch to use two spaces, and the other is reformatting every file they touch to use four spaces, there are going to be lots of spurious changes, and lots of merge conflicts.

To approach this, we may need to:

  • Disable our auto code formatters
  • Change the configuration of our auto code formatters (maybe by finding an existing configuration in the repo, or adding one)
  • Reformat the whole repo with a consistent configuration in one commit, and then keep it formatted.

We should never include spurious reformatting changes in the same commit as real code changes.

PR Descriptions

PR descriptions are even more important when working with Legacy Code. We probably had to do a lot of learning to work out how something works, what was wrong, and how to fix out. The person reviewing our change may not know the codebase very well either. The more detail we can write in our PR descriptions, the more information the next person has when they need to investigate something (and the easier it will be for our reviewer to review our change).

Make sure your PR description clearly describes what you learnt - how do things work, what was wrong, and how did you fix it?

Appetite for risk

When we’re working on brand new codebases, often we will make risky choices. Maybe we will use a new framework. Or a new language. Or decide to use a new serialisation format.

When working with legacy code, these choices often have more risk associated. There is lots of existing code - we may need to update lots of it, which means understanding it. And modifications often lead to bugs. Every time we rewrite some piece of code, there’s a chance we change its behaviour in an important way. Maybe the old code handled null and the empty string differently, and the new code treats them the same.

When working with legacy code, we often make conservative choices. We try to change the minimum we can to make the fix or feature we need.

Sometimes we can justify bigger changes, but doing so is a big decision we need to take seriously.

Familiarising Yourself With a Codebase

Learning Objectives

Before we can debug or fix anything in our new codebase, we need to understand what we have.

Different projects document things differently. Some common conventions are:

  • In a README.md file at the root of the repository.
  • In a README.md file in the directory of each component.
  • In comments across the codebase.
  • In a wiki, or standalone website, for the project.
  • In a file named DEVELOPING.md, CONTRIBUTING.md, or similar, in the root of the repository.
  • Assuming you’ll ask someone who already knows how things work to help you get started.

✍️exercise

Work out which of these approaches the Purple Forest application uses.

Sometimes documentation goes out of date! Most of the time the documentation was correct at the time it was written, but something changed in the application and no one remembered to update the documentation.

This is unfortunate, but the reality of the world. Sometimes there will be gaps. Sometimes you need to work out how to fill them. Sometimes you need to understand error messages to fill in those gaps. Sometimes you will need to ask someone. Sometimes you can look through the git history to work out what changed since the documentation was written.

✍️exercise

Work out what software you need installed on your computer to run Purple Forest, and how it’s intended to be run.

Getting Up and Running

Learning Objectives

Now that we’ve gotten to know the shape of our repository a bit, let’s try to run it. The goal of this is to end up with a running system which we can open in our browser and log in to.

You should have found README.md files in different top-level directories of Purple Forest. Follow their instructions to get all of the components you need running.

You may run into errors due to out of date documentation. You will need to debug these by reading error messages - you may even need to reach out for help from others.

When you have successfully opened the front page of Purple Forest in your web browser, and logged in as a sample user, continue onto the next step.

Finding things

Learning Objectives

By tracing request flows, drawing maps, and using code search tools, you can efficiently find your way through legacy code.

1. 🔍 Find

We’re going to use the features of our IDE to help us. VSCode has a million features for this, but we’re just going to start with four: Open file, Find references, Peek definition, and Find in files.

📂 1. Open file : Cmd+P or Ctrl+P

Code Along

Open the Purple Forest codebase. Let’s start at the router. Press Cmd+P and type router. Open the file router.mjs and look at where it shows up in the Explorer. What else is in that directory? Why have these files been grouped together? Write down your ideas.

What does this module do? What can it tell you about the system?

🏷 2. Find references: fn+Shift+F12

Code Along

In router.mjs there is a function called handleRouteChange. What is using this function? Where is it called?

Press fn+Shift+F12 . This will show you a references panel with links to every place that references this function. Double click on a reference to navigate to that file.

🫣 3. Peek definition: fn+F12

Code Along

Now you’re in index.mjs you can see where the function is called, but you can’t see the details of the function. Double click on the function name to select it and now press Fn+Option+F12. This opens the peek panel, which shows you the function definition without leaving the file you’re in.

🗃 4. Find in files: Cmd+Shift+F or Ctrl+Shift+F

Code Along

Is that everything to do with the router? Press Cmd+Shift+F and search for route. What else do you find?

Repeat this process with navigateTo. Deliberately practice using keyboard shortcuts to navigate your codebase. As the code you work with gets more complicated, scrolling through files becomes enormously time-consuming.

💡Tip

You will learn these keyboard shortcuts over time.

Use the commands now, but you won’t be able to remember all the keyboard shortcuts at once. Try to learn one or two more each week. Use the Command Palette to look up the keyboard shortcuts.


2. 📞 Trace a Request Flow

Open Purple Forest on your local machine. Read the README to get it running, and launch the frontend with Live Server. Again, Devtools has a million features but we’re going to use four: Event Listener panel, Network panel, Sources panel, and Local Storage in the Application panel.

Code Along

  1. Inspect the login form and find the event listener in the listener panel. Make a prediction. What will happen, step by step, when we submit this form? Write down your answer in a numbered list. It doesn’t have to be perfect, just jot down a quick prediction.

  2. Open the Network panel. Log in🧶🧶 Log inYou can find the seed login details in the codebase you are reading! as user sample. What do you see? Write down any observations you can make in bullet points.

  3. In your notebook, sketch the flow of the request from the user click to the server response. Complete this flowchart:

--- config: look: handDrawn --- graph LR A[Login form submit] -->|Event| B[handleLogin] -->C[?]

Note: Your completed flowchart should show a sequence from the user click to the UI update. Label the answers to the following questions:

  • What function makes a request to the server?
  • What is the endpoint?
  • What comes back from the server?
  • Where does that response go next?
  • What drives the UI update?
Some help if you are completely stuckLogin form submit --> handleLogin --> Sends form data to apiService.login --> Fetches token & success from /login --> calls updateState --> State updates, persists to localStorage, dispatches state-change event --> Router listens for event and --> calls Home View --> clears page with Destroy, then calls Render --> renders Profile, Timeline, and Logout components with current State

Add a console.trace(); to the home view to help you trace the flow.


3. 🖍 Sketch the System from different perspectives

The practice of sketching clarifies the mental model of the system in your mind. It doesn’t have to be a complicated drawing. The router module calls different “views”. Where are the views defined? What do they do? Here’s a quick sketch showing the relationship between the router and views:

--- config: look: handDrawn --- graph LR A[Router] -->|matches url| B{Views} B --> C[Home View] B --> D[Login View]

What about the relationship between views and components? Find the components that are called in the home view. In your notebook, draw your own diagram showing the relationship between views/home and components/*.

You could also sketch:

  • Data Flow Map: Illustrate how data moves through the system
  • Component Tree: Show the hierarchy of components
  • Dependency Map: Identify which modules depend on each other

Debugging: Proposing and Discarding Hypotheses

Learning Objectives

Bug Report 🔗

Bug report

👤 When I click on hashtags, the page flashes blank on and off

User provided details

  • User agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
  • URL: #/hashtag/do
(You could take a moment to refine the issue report. )

🧠 Recall that debugging is about forming and testing hypotheses. Each test brings you closer to understanding the system’s intended and actual behaviour.

Remind yourself of your debugging skills
--- config: look: handDrawn --- graph LR A[Predict] B[Explain] C[Try] D[Compare] E[Update] A --> B B --> C C --> D D --> E E --> A

You have used this strategy many times before at CYF, and loads of debugging too.

With the application running, reproduce the issue by creating a bloom with the hashtag #do and navigating to /#/hashtag/do. Open Devtools and trace the request flow, just as we did in Navigation.

Yikes! As soon as we open the Network panel we can see the app is making many many many requests!

Prediction: there’s some kind of loop in the system that’s causing the page to refresh over and over. Explanation: the network panel is showing a lot of requests to the same endpoint.

We can see precisely which files are involved in this request in the call stack. This stack trace allows us to reduce our problem domain to these 5 files.

_apiRequest @ //front-end/lib/api.mjs:33
getBloomsByHashtag @ //front-end/lib/api.mjs:163
hashtagView @ //front-end/views/hashtag.mjs:20
handleRouteChange @ //front-end/lib/router.mjs:24
(anonymous) @ //front-end/index.mjs:44
updateState @ //front-end/lib/state.mjs:26

Remembering what we just learned, Cmd+P and open api.mjs, Cmd+F to jump to _apiRequest. This is a wrapper function that all these endpoints call, so it’s not likely to be the problem if only this one view is refreshing.

  1. Read getBloomsByHashtag. Is there a clue in here?
  2. What is calling this function? Use fn+Shift+F12 to navigate to hashtagView. Prediction: if we comment out the apiService.getBloomsByHashtag call, the page will stop refreshing. Try it.

This should be a clue.

💡Tip

Legacy code is like a crime scene. Use your detective skills to understand how it happened.

Our expected request flow is:

sequenceDiagram title Expected Flow hashtagView->>apiService: Get blooms apiService->>Server: Request Server-->>updateState: Update State-->>Router: Event Router-->>hashtagView: Render once

hashtagView calls apiService.getBloomsByHashtag which calls _apiRequest which makes a request to the server. Success updates the state which dispatches a state-change event that the router listens for and calls hashtagView again to render the page with the blooms.

But our actual flow is:

sequenceDiagram title Actual Flow (Loop) hashtagView->>apiService: Get blooms apiService->>Server: Request Server-->>State: Update State-->>Router: Event Router-->>hashtagView: Render Note right of hashtagView: Loop starts hashtagView->>apiService: Get blooms again apiService->>Server: Request again Note right of Server: Endless cycle...

hashtagView calls apiService.getBloomsByHashtag which calls _apiRequest which makes a request to the server. Success updates the state which dispatches a state-change event that the router listens for and calls hashtagView that calls apiService.getBloomsByHashtag which calls _apiRequest which makes a request to the server…

This is where debugging legacy code can be faster than a greenfield application. This application is working, and other views don’t have this problem. So we can look at other views, and spot the difference. In views/profile.mjs for example, we call the apiService inside a conditional:

// Only fetch profile if we don't have it or if it's incomplete
if (!existingProfile || !existingProfile.recent_blooms) {
  apiService.getProfile(username);
}

This is different to how we are using apiService for hashtags. Maybe this is intentional, or maybe this is a violation of a pattern.

  1. Hypothesis: The hashtagView is calling apiService.getBloomsByHashtag multiple times.
  2. Test: Comment out the apiService.getBloomsByHashtag call in hashtagView.
  3. Result: The page stops refreshing… and is also blank.
  4. Conclusion: The loop is caused by hashtagView calling apiService.getBloomsByHashtag multiple times.

Uncomment the line and don’t make any further changes to the codebase. Move on to the next step.

Fixing: Targeted Changes with Test Support

Learning Objectives

The fixing cycle protects against regressions

--- config: look: handDrawn --- graph LR A{Identify Issue} --> B[Write Test] B --> C[Write Fix] C -->|Tests Pass| E(Document Fix) C -->|Tests Fail| F[Revise Fix] F --> C

We have identified our issue and written our test(s). We’ve really done all the hard work already. Fixing the actual code is now simple.

Fix your code!

If you are really stumped, here's the fix

On line 20 of views/hashtag.mjs, only fetch if the hashtag has changed.

if (hashtag !== state.currentHashtag) {
  apiService.getBloomsByHashtag(hashtag);
}

If you were tempted to write something like if (state.hashtagBlooms?.length), consider: what would happen if you navigated to a hashtag view with no matching blooms in the database?

Try it and look in the network tab. Yikes! Another infinite loop. A conditional written presuming there is always content available is risky with user generated content. You can’t rely on users!

What extra test could you write to cover this case? Write it.

✍🏾 Document your fix in your PR message.

Once you have got the entire fix working end to end with tests, open a PR with your changes. In your PR message, write everything that you needed to know to solve this problem. Be good to the reviewer, they’re a good friend of yours.

📅 Schedule a revision

Schedule a revision in your calendar for one week from today to come back and review your own PR.