Building a newbie-friendly codebase
Make your codebase easy for everyone to get acquainted with
By Pedro Santos
While in college, I was the typical student who ticked all the boxes in the evaluation form, knowing I would never have to revisit the code I was crafting.
After graduating, I was fortunate to work on a couple of greenfield projects, where I was part of all architectural and product decisions. Besides building products that ticked all functional boxes, I wanted them to be something that my future self would be happy to deal with.
Then, I moved to a company that had an established product with years of development. I had to do a myriad of tasks, from fixing support-reported bugs to convert legacy components from a soon-to-be deprecated technology to the new one everyone’s excited about.
All of these realities really highlighted the importance of having a codebase that not only works, but is also easy to maintain and onboard new people into. ** With more and more software engineering jobs becoming remote and async, you won’t always have a chance to clarify questions synchronously with your colleagues. You want to set up your code for people to be independent and be able to “fly solo”.
Here’s a set of simple tips that really make a difference on building a codebase that’s easy for everyone to get onboard into, understand and maintain. They’re as easy to apply as they are easy to forget, so always keep them in mind! Let’s dive in. 🚀
Convention everything
Have your code as consistent as possible, from architecture, to naming, to code styles. Whether the approach is perfect or just good enough, what’s most important is that your codebase is consistent.
It’s critical to be able to learn once and apply everywhere because:
- Everyone’s background and habits are different, so if you have established patterns, there will be less friction and less subjectivity when discussing solutions.
- It allows focusing on the content, not the form.
- When someone new joins your project, they won’t have to learn different patterns to understand different parts of the codebase, they just need to learn once, and they’re ready to go.
Examples
Layout names
Follow a structure for your layout names that showcases both technical and functional details, e.g.:

Comparison of uncontextualized with standardized layout files names.
Patterns
Let’s define an example of handling a button click. You can do it in several ways, like:
buttonToChangeColor.setOnClickListener { button ->
if (button.color == Color.RED) {
button.color = Color.BLUE
} else {
button.color = Color.RED
}
}
or
buttonToChangeColor.setOnClickListener { button -> updateColor(button) }
Which one is better? It depends. But if you’re consistent, it’ll be much easier to focus on what the click listener is doing rather than where or how it’s doing it, which is unequivocally more important.
Code styles
When code is organized and formatted in the same way, it’s much easier to concentrate on the content and find what you’re looking for.
When reviewing code or having your code reviewed, there will be less bike shedding and feedback will be more objective and fruitful, e.g., it doesn’t matter if you like trailing commas or not, if the convention is to have them, then you just have to follow that and not worry if the reviewer will nitpick on that.
Information flows in a one-way street
Have a single source of truth and unidirectional data flows. It’s easier to understand where data is created, propagated to, accessed and which actions can modify it.
Having multiple places creating or updating the same data makes the flow of information and cascading effects hard to understand, creating a highway for bugs that are hard to debug and fix.
Repel magic values
Extract all of your “magic values” to constants with clear names. This will make it easy for everyone to understand what those values mean!
An example:
val timeDiff = currentTime - lastLoginTime
if (timeDiff > 259200000) {
showWelcomeBackMessage();
}
What does 259200000
actually mean? Is it days? Minutes? This isn’t clear from the very start.
On the other hand, if you had this:
private const val MIN_TIME_SINCE_LAST_LOGIN_MILLIS = 259200000 // 3 days represented in miliseconds.
(...)
if (timeDifference > MIN_TIME_SINCE_LAST_LOGIN_MILLIS) {
showWelcomeBackMessage();
}
It’s much clearer what the value means in that context.
Use data structures in your favor
Data can be represented in many ways. Encapsulating all the attributes under a dedicated class, using just primitives, you name it! That said, some solutions might be more ambiguous or error-prone than others.
Always model your data as precisely as you can.
For instance, if you want to represent the state of a given component, you can do it with a set of strings, e.g.:
const val STATE_LOADING = "state_loading"
const val STATE_LOADED = "state_loaded"
// When analyzing the possible values you'd do something like:
val state: String = (...)
when (state) {
STATE_LOADING -> (...)
STATE_LOADED -> (...)
else -> // UNKNOWN STATE.
}
But if you do this, the possible values of state might not be clear for everyone, and you don’t guarantee that valid values are enforced, so you need to handle unknown / erroneous cases explicitly, etc.
A safer alternative to this would be to use, e.g., an enum:
enum class State { LOADING, LOADED }
val state: State = (...)
when (state) {
LOADING -> (...)
LOADED -> (...)
}
With this approach, it’s much clearer what are the possible values for state are, there’s no chance to have values outside the expected domain, and if there’s a typo you’ll have a compilation error. This code is safer to handle and easier to understand.
For more on this topic, watch this excellent talk by Christina Lee from KotlinConf 2018 about Representing State .
Test it, test it, test it
Ah, the famous last words: “We need to ship this ASAP, we’ll add tests later.”
The truth is that we all acknowledge the importance of tests, but sometimes discard the long-term benefits they offer for the sake of short-term velocity.
Let me tell you a quick (horror) story: not long after having joined Doist , I was working on fixing a bug on Todoist ’s main task list. The bug was relatively simple, and it had to do with collapsing and expanding Sections. I found a way to reproduce the bug and came up with a fix. This fix found its way to production, and I was happy with it. If only I had known that the fix that I was so proud about didn’t go “alone” to production but took a little fellow with it: I ended up breaking collapsing and expanding of tasks. So, when the support reports started coming in, we had to halt the release, do a quick-fix on top of my initial fix and do another release. A major hassle that could’ve been avoided if we had tests that helped catch the bug I unknowingly just introduced.
When you have tests in place, it’s easier for everyone to catch and understand the collateral effects of what they’re doing and much safer to validate the change’s they’re introducing.
This is especially significant when people aren’t aware of all the previous considerations a particular piece of code required, like in the example above. If you encourage and foster adding tests for each bug that’s fixed or each feature being added from the very beginning, it’s much easier to keep the ball rolling, to keep your test suite up to date and, most importantly, useful.
Comments are good, but good code is even better
The feeling towards code comments is similar to tests. Everyone knows they’re useful, but… There’s always a but. Outdated comments can be very misleading and confuse their readers.
So, even though having comments, in theory, is great, try to reduce them to the bare minimum, so it’s easier to maintain them. Tests fail loudly but outdated comments fail silently, so focus on having code that’s easy to read as well as up-to-date and meaningful tests.
You might be wondering what ”code that’s easy to read” actually means and when you should add comments or not. Of course, it depends on the context and on whoever’s “classifying,” but a few personal rules of thumb are:
- Comments usually explain the why of a certain piece of code, not the how, as that’s redundant and quickly becomes outdated when you refactor that code.
- If you use established patterns, the reader will most likely have an easier time understanding a piece of code without wishing for comments.
- Use property / method / class / file names that are clear and self-explanatory in their purpose.
- In general, if you’re following this article’s suggestions, you’re on the right track. 😌
Acronyms are B.A.D.
Acronyms Seriously Suck . They’re only great for people who are comfortable with your glossary.
Engineers love optimization, so there might be a tendency to write fewer characters and choose “obvious” acronyms in favor of the full version. But the new kid on the block won’t know the glossary and will be left wondering when you say that your PDP has a bug. It’s obvious that you’re talking about your Product Detail Page, but someone else may be thinking why on earth their Personal Development Plan has a “bug”.
Instead of using PDP
to refer to your ProductDetailsPage
, use its full version. Characters are free, and it’ll be clear for everyone what you’re referring to.
Refactor opportunistically
Keeping a codebase consistent is hard! It also gets harder and harder as time goes by, new patterns get introduced, and people come and go. You have to be intentional about your work.
When working on any part of the codebase, always try to leave it better than you found it. Take that chance to update a parameter name that’s off, to apply that small update you did to the codestyle last quarter, or to update an old component to your new architecture pattern.
Being intentional will get you closer to long-term success than just “going with the flow.”
Congratulations on getting to the end of the article! Now you might be wondering: “Yeah, all of these tips are reasonable, but aren’t they a little obvious? Aren’t they standard?” Yes, they’re obvious and yes, they should be standard. But the fact is that more often than not, they’re not.
So, always keep the following in mind: “If I was just coming to this codebase, what would facilitate my learning curve?” and keep on improving it. Incentivize newcomers to give feedback on the codebase, your processes, your techniques, and act on that feedback.
One day, you’ll be the newcomer in someone else’s codebase, and you’ll be grateful they kept this in mind. 💡