- Published on
Yet Another List of Random Opinions on Writing Readable Code and Other Rants
- Name
- Luca Cavallin
After more than a decade of writing software, I've read lots of opinions on how to write good software. Everyone loves to throw around rules and principles, so I'll do it too. Writing readable code is about making life easier for the people who have to deal with it later - not about flexing your cleverness or following trends that sound smart on social networks.
This is just a list of thoughts I've gathered over the years, thrown together in no particular order. They are not a definitive guide, there are plenty of those to be found online. Do with this what you want - agree, disagree, or roll your eyes. But if you're looking for practical, no-nonsense advice (with a little side-eye for the fluff we all put up with), you're in the right place. Writing readable code isn't that hard - once you stop overcomplicating it. I've include some examples in Go. I use Go the most so bear with me.
Avoid Small Packages
Look, splitting your code into a million tiny packages might sound like a neat way to organize things, but in practice, it's just annoying. Every package is another layer of cognitive overhead. You're forcing anyone who works on the code to jump between files like a "chicken without head" just to understand a single feature. That's not "modular"; it's torture designed by a sick mind.
Ask yourself: "Do these things actually belong together?". A package should group related functionality, not serve as a dumping ground for your overzealous need to compartmentalize.
Example: Group Related Logic
Bad:
/myproject
/validators
validator.go
/parsers
parser.go
/services
service.go
Good:
/myproject
/processing
validation.go
parsing.go
service.go
See the difference? Stop scattering your logic across directories for the sake of it. Cohesion is the goal - not package proliferation.
util
Packages (and Maybe Even Utilities)
Avoid Ah, the infamous util
package. Every codebase has one, and it's always a mess. Let's be real: a util
package is just a fancy name for "I didn't know where to put this, so here." Over time, it becomes a dumping ground for random functions that have no business existing in the first place.
Here's a radical idea: do you even need that utility function? Before you write yet another "helper", check if an open-source library or the standard library already does it. Chances are, someone else has already solved the problem better than you will in 10 minutes of hacking.
Example: Don't Reinvent the Utility
Bad:
func Contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}
Good:
import "slices"
func main() {
if slices.Contains([]string{"a", "b"}, "a") {
fmt.Println("Found")
}
}
The Go slices
package exists. Use it. Stop trying to play hero.
Just Make It Testable
If your code is hard to test, congrats: you're doing it wrong. Writing testable code isn't just about tests - it's about good architecture. If your code is modular and decoupled, it'll be testable and easier to maintain. If it's a tightly coupled spaghetti mess, no amount of mocking is going to save you.
And this is pretty much all I have to say about good software design.
How Much Coverage Is Enough?
Ah, the age-old question: "What's the ideal test coverage percentage?". How much of your code runs in production? That's how much coverage you want. But don't waste time testing meaningless getters, setters, or dead code. Focus on the stuff that matters:
- Critical business logic
- Edge cases
- Failure paths
Example: Testability Means Flexibility
Bad:
func FetchData() string {
return externalServiceCall()
}
Good:
type Fetcher interface {
Fetch() string
}
func ProcessData(f Fetcher) string {
return strings.ToUpper(f.Fetch())
}
Mocking Fetcher
in your tests makes this code infinitely easier to validate. Hard-to-test code is a red flag that your design needs work.
Repeat Yourself (a Little) and Minimize Jumps
"Don't Repeat Yourself" (DRY) is great - until it isn't. God-tier abstraction for the sake of avoiding repetition often leads to code that's impossible to follow. Every time a developer has to jump to another file or helper function, you're adding friction.
A bit of duplication isn't a crime if it makes the code easier to read. Keep related logic together and stop scattering trivial functionality across a dozen helpers.
Example: Avoid Pointless Abstraction
Bad:
func Process() {
start()
// more code here
}
func start() {
setup()
// more code here
}
func setup() {
prepare()
// more code here
}
func prepare() {
fmt.Println("Processing...")
// more code here
}
Good:
func Process() {
fmt.Println("Processing...")
}
If a new function adds little value, don't create it. Less jumping around means easier debugging. And if you're worried about duplication, focus on the duplication that matters: business logic that requires consistency over several steps. Everything else is just noise.
A case can be made for when functions are a replacement for comments. For example, a function used in an if statement to make the code more readable. This is a good use of a function. Example:
func main() {
if isUserAllowed(user) {
fmt.Println("User is allowed")
}
}
func isUserAllowed(user) bool {
permissions := getPermissions()
hasPermission := false
for _, permission := range permissions {
if permission == "admin" {
hasPermission = true
}
}
return user.Enabled
&& hasPermission
&& user.Domain == "example.com"
}
A forced example I should say, but you get the point.
Don't Write Useless Docs
Documentation is important, but let's not pretend that all docs are helpful. Copy-pasting docs from a third-party project into your repo because maybe maybe you might need some information at some point? Pointless. Writing verbose inline comments that restate what the code already says? Equally pointless. Important information spread across a dozen READMEs, wikis, and Google Docs? Pure evil.
Write useful docs:
- A solid README to get people started.
- Clear explanations of non-obvious design decisions.
- Troubleshooting guides for common issues.
If your documentation doesn't actively help someone, it's noise.
Do Not Reinvent the Wheel
There's almost never a good reason to build something from scratch that's already been built. Your code isn't special. The world doesn't need your custom API router, logging framework, or queue implementation. Established libraries exist for a reason - they're reliable, well-tested, and familiar to your teammates.
Now, I know what you're thinking: "But adding dependencies is bad!!!11!1!!!" Sure, dependencies come with some risk, but your custom solution is almost always a greater liability. Your one-off implementation is untested by the broader community, lacks years of iteration and bug fixes, and ties your team to maintaining it indefinitely. That's a far bigger risk than relying on a library supported by hundreds (or thousands) of developers.
Examples of Reinventing the Wheel:
- Writing your own API router instead of using
gorilla/mux
orchi
. - Rolling your own task queue instead of using Redis or RabbitMQ.
- Building a custom logging library instead of adopting
zap
. - Anything that already exists, really.
Even if your solution is technically better (and let's face it, it probably isn't), it adds unnecessary complexity, isolates your team, and makes it harder for others to work with your code. Use the tools that the community has vetted, and focus your energy on the parts of your application that are truly unique. Your custom router isn't it. Stop it, go touch grass.
Technical Problems Require Technical Focus
No amount of meetings, roadmaps, or tickets is going to fix your tech debt. Process and management can help organize the work, but the work itself is inherently technical. You can't company-retreat your way out of a tangled codebase or a buggy deployment pipeline.
Now, let's talk about business value. Yes, it's important - critical even - but let's not forget our job as engineers. Cutting engineering corners for short-term gains might look good on paper for a while, but history is littered with cautionary tales. Boeing's planes generated a lot of business value… until they started falling out of the sky because someone decided engineering rigor could be traded for faster delivery. As engineers, we have a responsibility to balance business needs with long-term technical health. Business value doesn't come from burning down your systems - it comes from building things that last.
The solution? Stay focused on solving technical problems with technical solutions. That means prioritizing time for actual work: refactoring messy modules, improving test coverage, and cleaning up hacks you promised to revisit "later", for example. Discussions, meetings, and processes should only exist to move the work forward - not as platforms for self-promotion or endless deliberation. If a meeting doesn't provide actionable insights or help solve a problem, it's noise.
Less noise, more technical focus, and commitment to engineering quality ensure that progress happens where it matters most: in the code - and in ways that don't compromise the future for the sake of the present.
Learn the Tool
One of the fastest ways to write unreadable, unmaintainable code is to jump into a new language or framework without bothering to learn its idioms. Every tool has its own set of conventions, patterns, and best practices. Ignoring these because "this is how I did it in [insert your last project or favorite language]" will lead to code that feels awkward, inconsistent, and out of place.
Writing crappy code while learning is fine - great, even! That's how you experiment, fail, and improve. But once you move past the basics, take the time to actually understand the tool you're using. In Go, for example, this means embracing simplicity, idiomatic error handling, and conventions like returning concrete implementations instead of interfaces. Don't try to shoehorn in patterns from Java, Python, or whatever you used before - it just makes the code harder to follow for anyone familiar with the language.
Spend time reading documentation, open-source projects, or idiomatic codebases. Ask yourself, “How would someone who knows this tool well solve this problem?" Your goal isn't just to make the code work - it's to make it work in the way the tool was meant to be used. Learning the tool well doesn't just make your code better; it also earns you credibility with other developers. No one wants to maintain a Go project that looks like a weird mishmash of Java-style patterns.
In short: take the time to learn and respect the tools you're using. It's an investment that pays off for both you and anyone who has to work with your code later.
Summary
Readable code isn't just about technical skill - it's about empathy. It's about recognizing that your work will be read, maintained, and built upon by other people (including future you). Make their lives easier.
Clean up regularly, write useful tests, avoid unnecessary complexity, and make thoughtful decisions. If you do, you'll not only write better code - you'll build better systems, better teams, and better software. And that's a win for everyone.
But remember: there's no one-size-fits-all solution. Every project, team, and situation is different. Use your judgment, adapt to the context, and don't be afraid to experiment. The best practices of today might be the anti-patterns of tomorrow. Stay curious, stay humble, and keep learning. That's the only way to write code that stands the test of time.