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:
- You didn’t write - It lacks your mental model and intentions.
- Powers important systems - It can’t simply be deleted or replaced.
- 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
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
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
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
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.
Open the Network panel.
Log in🧶 as user🧶 Log inYou can find the seed login details in the codebase you are reading! sample. What do you see? Write down any observations you can make in bullet points.In your notebook, sketch the flow of the request from the user click to the server response. Complete this flowchart:
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 stuck
Login 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 StateAdd 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:
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
👤 When I click on hashtags, the page flashes blank on and offBug Report 🔗
Bug report
User provided details
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#/hashtag/do
🧠 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
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.
- Read
getBloomsByHashtag. Is there a clue in here? - What is calling this function? Use
fn+Shift+F12to navigate tohashtagView. Prediction: if we comment out theapiService.getBloomsByHashtagcall, 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:
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:
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.
- Hypothesis: The
hashtagViewis callingapiService.getBloomsByHashtagmultiple times. - Test: Comment out the
apiService.getBloomsByHashtagcall inhashtagView. - Result: The page stops refreshing… and is also blank.
- Conclusion: The loop is caused by
hashtagViewcallingapiService.getBloomsByHashtagmultiple 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
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.