Ask HN: What are the ways you go about getting comfortable with a new codebase?

59 points by jarusll ↗ HN
Tools like Eclipse's Java Browsing Perspective(https://querix.com/go/beginner/Content/05_workbench/01_ls/02_interface/01_perspectives/java_browsing.htm), Visual Studio's Object Browser make it easier to browse the codebase by components.

What are the ways you do it? Is there anything similar available for functional programming?

68 comments

[ 2.5 ms ] story [ 150 ms ] thread
(This might be more abstract than you wanted)

Hopefully there's an overview of the code base in an `ARCHITECTURE.md` file[1], and then read through it, and the respective documentation and tests for the main modules mentioned in it.

If you assume their tests cover the important business logic / stuff "they want to keep" (ref. "Beyonce Rule"[2]), they should inform you about the most important stuff.

> [1] https://matklad.github.io/2021/02/06/ARCHITECTURE.md.html

> [2] https://www.oreilly.com/library/view/software-engineering-at...

Ideally you have someone experienced in the codebase who can give a lay of the land.

I suggest:

1. Find a senior dev, ask then for exisiting pointers to good documentation to self learn.

2. give that a go, make note of all the questions you have

3. then have a session with that dev for platform walk through. Take lots of notes and ask your questions.

4. offer to update docs where you found errata or missing steps or even complete topics not mentioned

5. suggest to the team anything about onboarding that can be improved.

This is exactly what I would do ideally except I couldn't. I can understand that working in startups you would overlook alot of theory and due to hectic nature low quality calls(code/architecture explanation) are not appreciated.

And that's why the first thing I ask during a technical interview is "Do you have internal documentation?".

Fixing bugs.

Someone can explain a code path, what it should do, what the bug is and with that you can get familiar with a path through the application.

This!

Ask around about the big things everyone would like to change and where the scary code is that nobody wants to touch, and with those things in mind, fix some bugs.

The initial questions will tell you what to avoid initially. Longer term, if you can fix them you'll look like a rock star.

Fixing bugs has worked out although I had to pull my hair.
Run the code. This sounds so trivial but it's not always that easy. Once it get's running, I discuss with users of the code (be it customers or developers) and try to understand what they use it for and look for that same functionality in the code. Usually by this point I know where everything is and how to make changes if needed.
Yep, underrated advice. Run it, read the code, fiddle some bits and make sure what you fiddled matches your mental model.

Honestly, learning to read code and execute it in your head is a super power

Even better than running code is running tests.
Run it under debugger and go thru whole codebase a few times and you'll start getting proficient at it

I've recently did it on huge, very very specific codebase and after 2-3 months I understood it (what to add where, not just what's happening where) relatively OK

With both lambdas and @ComeFrom instructions[1], it’s impossible. I’m impressed that a feat of modern programming (say, started with Java 8) makes the code difficult to understand.

[1] https://en.wikipedia.org/wiki/COMEFROM

I have never heard of COMEFROM.
I love debugging and I would prefer this even if it's slower but there are cases where it's not easy to setup a debugger.
Not using things like go to definition and any fancy tools, just manually forcing myself to work through files to understand how things fit together and using basic tools like grep
Very surprising! IIUC, you consider "Go To Definition"/"Go To References" and other "LSP assists" unhelpful (or worse) when familiarizing yourself with a new codebase. I personally find them indispensable. Could you say more to help me understand your position?
If I start with these tools I get a feeling I understand how things fit together, but when I want to add something new I realize my understanding is pretty shallow. I force myself to manually open files where things are so I get an understanding of the conventions and principles in the project. By far my most valuable tool is grep (it’s super fast in vim) and I grep the code to see where certain functions and such are used. I use the lsp tools later when I’m used to the project.
I'd give the complete opposite advice.

I use "go to definition"/"find references" to go all around the code base and at the same time try to figure out how each files interact with each others.

I would prefer LSP assists as mentioned above. I love having things within reach. I believe programming should be as painless as possible.

Pharo(Smalltalk) IDE is one of the best IDE I've ever used. It even has a feature where you give input object and output object and it tells you what messages you need to send to get from input to output.

Checkout the repository and run the unit tests. If none exists, write the first one.
Besides all the great steps below, I like to browse the git repo to find the files that have been changed most recently and most often.

Projects, especially messy ones, often behave like lava flows where there is an active and ever expanding edge where changes are currently being made. Beneath this are layer upon layer of nearly impenetrable and often implicitly deprecated code from former developers.

This practice came from a time when I was brought in midway through a rewrite to get rid of unmaintainable code from some offshore contractors. I saw a repository where half the code lacked any organizing principles and had massive security issues. The second half was textbook (pedantic even) OOP, the kind taught in Java textbooks. It was beautifully executed except for using a few outdated tricks to do OOP in early versions of PHP (no longer needed in the version used for this project).

Because I didn't look at the dates, I assumed the neat OOP code was the result of the rewrite. I was wrong.

(comment deleted)
I often write out callstacks/dependency chains on paper. I find that makes it stick.

Try actually using the program as an end user would.

Read error messages, read code, make predictions about what the code does, find out if your predictions are true.

If there is one, I often like to look at the database. The stuff that is stored and the names of the tables should give you a good idea how the application works.

Sort of unrelated, but I've got a story about a project I was looking through that confused the hell out of me. It was a C# library that would allow you to render an element from a shockwave flash file (it was either .swf or .fla).

I spent ages digging through the code. The example worked really well, but I couldn't get it to work with one of my files.

Eventually I contacted the author and he told me the library used reflection to get the name of your variable and would look for that variable name in the flash document.

I like to focus on the main business element. If it's a SaaS for sharing videos with comments, for instance, I'd take a longer look at the video and comment models, their relations, and the call chain from API endpoint to model.

Another strategy I like is picking parts of the codebase and trying to refactor them. You don't even need to commit anything if you're not supposed to go around changing things: just by spending some time moving things around, seeing what breaks and so on will give you a better understanding of the code and what it does.

Refactor something unimportant with tests and a functional change, improve the documentation in the README, and push straight to the repo without a feature branch.

Reading the codebase does surprisingly little for me, you essentially have to change it and see what happens.

Three ways...

1. I go straight to the entry point, the main(), and then follow how the initial configuration, flow of data, sanitisation, and routing is done.

2. I look for bugs. Fixing bugs reveals the complexity as you need to look for side effects of the fixes when you don't yet know the system. Writing tests for those fixes also helps understand the system.

3. I look for the least changed part. I find these are usually the oldest and most core part of how the program works, whereas more recent changes are business logic and feature addition.

But of these, the first yield the greatest initial understanding and allows me to change things with less fear.

1st is exactly what I do while reminding myself that I don't need to understand everything. Somehow for me it's easy to think like that in OO code and not FP.
These are great.

A 4th way I would add is: If you need to make a minor change or understand how one specific function is expected to work, search for its unit tests and start there.

How do you think of #1 in the context of a web app? Each page is essentially a different entry point into the code.

I guess the landing page or authentication page could be considered the equivalent, but I’m not sure those would hit your goals to understand flow of data, etc. ?

It makes even more sense for web apps. It will tell you how it came to be that a web page received the request, what context it has, how the cookie is resolved to an auth context, how to access any query args, what is available to support the page.
Yup, I've stopped looking for the entry point in SPAs and apps. Often the navigation flow can be extremely convuluted.

Android introduced navigation graphs [1], which were meant to solve this problem. But what happened in reality were instead of arrows point to each screens, there would be multiple islands that were teleported in from random places.

[1] https://developer.android.com/guide/navigation/navigation-ge...

#1 is my method too. I also take it as a sign learning the code base is going to be difficult if I have to ripgrep to find main because they couldn't put it in a cleanly named file like main.c =)
The best way is probably to make changes to it because that forces you to really understand the code. If you just read it without making changes it's too easy to pretend that you understand it. If there is one part of the code that you are trying to understand, I can recommend to write a replacement for it; then you at least learn how it works and you might even get a better solution.
Two things - First - I learn how the data flows from the source to the end. That teaches me to navigate the codebase entirely. (User action to database, or source data to end data etc.

Second - I learn how different components are wired together.

So indexing all the components and finding out all the interactions between them. This is exactly what class browsers do, they index all the classes and messages. The interactions could be described using an example/documentation.

This reminds me of Pharo which does all of the above, indexes classes, messages and has a rich documentation support.

Get it running.

I find the only way for me is to actually run the code locally, play around with it until I understand the data flow.

Try to profile the code and see where it's spending time.

Get a good flame graph up, and you'll have a really solid visual representation of what's going on.

Bonus: on almost any project, nobody has done a profiling pass in at least a few months, so you'll probably discover some extremely easy performance improvements and you'll look like a goddamn hero when you speed up e.g. the test suite by a factor of 3 in your first week on the job.

Definitely a good advice. Profiling is something I haven't looked into and should be after debugging.
I did this and flamegraphs in Rust are not great due to rayon :(
It's counterintuitive, but stepping through a very complex codebase with a debugger and taking notes along the way was great in my case.

Also, attempting to draw a sequence diagram and fixing it as you go trains the brain to handle a mental model of a large process.

Apart from reading, and trying to understand the architecture, these are some things you can do:

1. Fix a couple of bugs

2. Add a small feature

3. Refactor a small piece of it

Always start with few small things and keep increasing the complexity of the things you do. Working on a codebase is the best way to understand it.

Learn the main scenarios of the product and try to walk through them with a debugger.

Also try to read tests, because they can show a lot about how the components are used and their properties.

I scan everything that is closely or remotely related to the code base: the code, commit logs, diagrams, bug reports, change requests, user manuals, tests, technical logs, databases, other storage, cloud infra, ...

Usually at least one of them stands out, so I at least read this through (usually diagonally).

I might also pick different things based on my goals.

Once I think I have a grasp of the high level aspects, I start pairing or validate with tiny feedback loops.

Update: I also create my own (naive) helicopter view diagram of the context and validate it with people on different levels.

Depending on the codebase of course. But for me, mapping out the datastructures and understanding their relations helps the most.
Just mapping the whole codebase without a specific goal in mind seems counterproductive to me.

Instead, I get myself a couple specific tiny bugfixes/features to do first. Just finding out where those are, one by one, tells you a lot and may not be as simple as it sounds.

I was once hired to help with polishing a code base for imminent shipping. I fixed one bug. The fix was one line, but not trivial at all. Took me a whole week of reading code. The customer was extatic. There were like 12-15 years worth of layers of code to read.

If the codebase is ginormous and hard to decipher then you could use the magic source control to go back in time to an early point in the codebase. It’s probably going to be easier to understand a codebase that is 3 months old vs 6 years old, so you could go and check out that version, understand it and then jump forward a few years. This also gives you the benefit of understanding the evolution of the code and understanding why it is not just what it is.
Get your team to produce easy tasks (usually refactors) to help you ramp up on the codebase