
How to write Readable Code? Beyond the Obvious

We all aspire to write readable code. Being experienced programmers, we have a good grasp of how to do it. Or so I thought. However, I first really understood what readable code means when I listened to Venkat Subramaniam’s following talk years back ( look at the next five minutes in the above talk).
Yes, readable and descriptive names and other best practices help, but keeping the right level of abstraction is the key.
As the video explained, when someone asks what you did at the weekend, he is not asking for everything you did, but rather looking for a high-level statement like we stayed at home, visited parents, or went on a trip. They would ask follow-up questions if they wanted to know more about a specific part.
The same idea applies to the code as well.
Our code is composed of procedures — an entry procedure that calls subprocedures, subprocedures that call the next level, and so on. Each procedure implements an algorithm.
In a readable code, the entry procedure should show the high-level algorithm, demonstrating the overall algorithm while hiding details that do not affect the high-level understanding. This demonstration is done by leaving clues about what it does in the code via variable names, procedure names, comments, and logic you write at that level. It is an art to leave enough breadcrumbs but not too much that will create clutter. Then, each sub-procedure should show its algorithm while hiding details that do not affect that procedure-level understanding.
Just like you do not want to enumerate everything you did when asked about the weekend, you do not want to show all the details at the entry level. At the same time, you must not hide details that are key to understanding the current flow.
Getting this balance is the key to readable code. If you do it right, the subsequent programmer who will wrestle with your code can read what is happening at each level. He can find the entry point and then go deep as he needs to fulfill his task at hand. It will save a lot of her time.
Let us take an example. This is a simplified code from a real example. This procedure is the entry point for a build controller, whose job is to carry out the build and deployment. Since both build and deployment take time to finish, the code is written as a control loop, not as procedural logic.
Loop performs actions based on its current state.
func performBuildAndDeply(context Context, build BuildRequest) {
lastStatus := *getLastBuildStatus(build)
switch lastStatus {
case "init":
workflowID := *triggerBuild(build)
updateWorkflowRunId(workflowID) //this will updata status to run
log.Println("Workflow started", workflowID)
case "running":
log.Println("Wiating for workflow to finish", workflowID)
context.workqueue.addAfter(build, 5*1000)
//when build completed it will update the status to sucess or failed
case "build-success":
createDeployableArtifact(build)
updateBuildStatus("completed")
case "completed":
return
case "failed":
notifyFailure("build faild", build)
default:
notifyFailure("unknown status" + lastStatus, build)
}
}
The code overall looks good and readable, but I see several improvements using Venkat’s lens.
- Having logs at this level is a distraction. It is better to push them to subprocedures unless they explain something that makes sense at this level.
- I would do all state changes related to the logic in this function at this level ( or via a clear method like updateBuildStatus(). What is happening in updateWorkflowRunId() is unclear. It would surprise the reader. I would pull that state update to the parent method.
- To understand context.workqueue.addAfter(build, 5*1000) at this level, the user needs to understand about the work queue. I will put that under a function that tells what it does, called scheduleLater().
Each of those try to get the right level of abstraction. The goal is to make it easier to understand the algorithm while eliminating or reducing the need for the reader to dig into subprocedures to understand what is happening. The improved code is given below.
func performBuildAndDeply(context Context, build BuildRequest) {
lastStatus := *getLastBuildStatus(build)
switch lastStatus {
case "init":
workflowID := *triggerBuild(build)
metadata := map[string]string{
"workflowID": workflowID,
}
updateBuildStatus("running", metadata)
case "running":
context.workqueue.addAfter(build, 5*1000))
scheduleLater(build, 5*1000)
//when build completed it will update the status to sucess or failed
case "build-success":
createDeployableArtifact(build)
updateBuildStatus("completed")
case "completed":
return
case "failed":
notifyFailure("build faild", build)
default:
notifyFailure("unknown status" + lastStatus, build)
}
}
We can summarize these as following best practices.
- Each method should demonstrate the right level of detail to understand its algorithm and put other details into subroutines.
- Minimize the surprise inside the subroutine. (e.g. Only do things function name says that are critical to the flow. Update to critical state or control flow, such as engaging a lock, should be done in the top level, not in sub-routines)
- Make high-level flow and decisions apparent
This may seem like a simple idea. However, understanding the need to demonstrate the right abstraction in each procedure, greatly impacted how I wrote my code. I rarely get this detail right in the first draft, but it is often an easy change when you come back to the same code after a couple of days.
Hope this was helpful. If you enjoyed this post, you might also like my new Book: Software Architecture and Decision-Making. You can find more examples from the book.

Get the Book, or find more details from the Blog.
Please note that as an Amazon Associate, I earn from qualifying purchases.