After finding myself in surprisingly unfamiliar territory at a new job working on a new domain ins a new programming language to me, I spent a bit of time asking myself, how do I know if this is good code and I should use it as a mental model for absorbing idioms, or if this is code written under some form of duress?
Idiomatic code
Idioms are important in programming. There are often many ways to solve the same problem, but a language may lean towards a particular solution because of either syntax, the standard library, or good 3rd party libraries and a long interesting history!
You can solve the problem in another way, but other developers who come across the code will be confused.
So, is idiomatic code good code?
I would say no. I have written some code in C# in the past which I consider to be good code, but not particularly idiomatic code. As in it’s not the way that they teach you to write C#.
Good code knows when to stick to the idioms. And it also knows when a better solution, a more domain specific solution, makes more sense. When you write idiomatic code, you should be going with the flow. If you are trying to write something in an idiomatic way, but having to battle a situation to do so. Consider your options.
The Idioms of the Domain, are more important than the Idioms of the programming language.
Folders & Files - Structure Has Meaning
Code is organised into methods or functions, data structures, and those are organised into files. Those files are put into folders. Sometimes these folders make up a namespace or Module system of some kind, forming an implicit/explicit hierarchy in the program.
The exact ways that these are organised, relates to the Engineer trying to find a balance between Cohesion and Coupling.
Not enough Cohesion, and code that has nothing to do with the rest of the code is all mixed in together. Too much coupling, and all of the code to do with a flow or concept is so cohesive that it does not allow for change moving forward.
Hopefully this explanation makes sense, you will find that the concepts are highly related to each other, and run in parallel.
You want High Cohesion, and Low Coupling.
Going back to our Idioms example, a lot of idioms in object oriented programming language pull you away from this goal! In many Object Oriented languages one is expected to organise code into horizontal slices, where the code in a class may relate conceptually as in they both operate on the same data-store, but when you look into how the program is ran and executed, the flow is not cohesive at all! One is forced to traverse many layers and stitch things together in one’s head to fit the puzzle it all together. Just to understand the flow.
This means that there may be multiple ways of achieving High Cohesion. Should your application be cohesive around concepts? i.e. store everything to do with Bananas in one location. Or should it be cohesive around flows? i.e. store everything about Banana procurement together, but in a separate place from the code that relates to Banana maintenance.
Personally, I prefer the latter. Which leads us to Virtual Slice Architecture. A topic which I should probably do a separate blog-post about. I am a big fan of this approach, I can save the details and my reasoning for another post.
Predictability & Determinism
Back to our original question, of what makes good code.
Good code is predictable
For code to be predictable, there are a few sub-properties we can thinking about. It should be Deterministic. It should also read from top to bottom, and have a solid Domain layer which describes what the program is doing in Domain terms, without mixing in dirty implementation detail. This is similar to the Transaction Script pattern. Good code has a Domain layer which has a collection of functions, which are stateless descriptions of a procedure, read from top to bottom, describing the process in domain specific terms.
Good predictable code does not require comments to tell the reader what is happening. It is written in a descriptive way. Consider the following examples
// Calculates the length of the Hypotenuse of a triangle
// where `a` & `b` are the lengths of the sides on the right angle sides of the triangle.
func pythagoreanTheorem(a, b float64) float64 {
return math.Sqrt(math.Pow(a, 2.0) + math.Pow(b, 2.0))
}
Versus
// Uses the Pythagorean Theorem to calculate the Hypotenuse Length of a triangle given the two other sides.
func hypotenuseLength(sideA, sideB float64) float64 {
return math.Sqrt(math.Pow(sideA, 2.0) + math.Pow(sideB, 2.0))
}
Yes, the example is very contrived. But I hope that it has helped you to understand how I think good code reads.
The astute reader will notice that these two functions contain the exact same information, just flipped between being in the code vs. being in the comments.
There are many more components to the predictable component. But to try to be terse, I will summarise it as code which you can read and clearly understand what it does and why it does it. You should be able to imagine a request coming through the system, and easily flow through the system without too much branching or cyclomatic complexity.
Good code does not surprise.
Testing
Good code is tested
We have all heard this one as a measure of legacy code as well, “Legacy code is any code without tests”.
I would take this one further, good code has good tests. What makes a good test? I’m glad you asked. First of all, for unit tests we must decide on what the appropriate size of unit is. I disagree that a class or a method/function is the correct size of unit for most programs. An application which is thoroughly tested purely at the class level is extremely brittle. Any change requires mountains of tests to be changed, and for the developer to understand how all of the classes all fit together and compose and how this change will ripple across all of the other classes. Because they have all mocked out their dependencies.
I instead propose that a good test focuses on testing an API (Application Programming Interface). Meant in the original definition of the term, not the modern RESTful HTTP API meaning.
Depending on the size of your program, this may be the same thing. If it is a small program just dealing with one thing, ran by one team, testing at this API makes sense.
You want these tests to exist at the Module level where the Module is used by some other person or team. If you have a large application, this may be a namespace/Module within the program. You may decompose a more complex program down into Modules even though it is all only used internally by your team. Testing at the Module makes for a robust program which you can refactor, and even replace with a high level of confidence. I could go on, but let’s save that one for it’s own post.
How easy is it to get rid of or change?
I always tell my teams that the only time that a project is finished is when it is eventually retired.
It may get retired for a number of reasons. Perhaps it was the wrong thing to build, so we put it down and walked away. Maybe it was the right thing to build to get us to where we are today, but now we need to do something different.
Good code is easy to delete
For code to be easy to delete. It needs to be well understood. The implications of removing it should be obvious, or documented somewhere. It should be easy to do so in some feature-flagged way that is safe to roll out. A lot of this will fall out of the previous properties we have discussed. If code is organised in a sensible way where there are obviously no side effects, and its domain purpose is well communicated in the code.
Code which is easy to get rid of, is also hopefully easy to change. Tying back into testing, if your units are appropriately scoped to the module level, you can very easily refactor / change your module and have a powerful regression suite at the ready.
Failure is inevitable
When our programs are running, they inevitably fail in some way. It may be some expected / planned way. Or it may be in a completely novel way we have not thought of. Either way, it will happen. Especially one you are running at scale. Once your program processes ~11 requests per second. You will hit a 1 in a million event every single day! That’s a lot of weird things going wrong!
Good code degrades gracefully
I have a lot of thoughts on Exceptions, which again should be its own post. But Exceptions should be Exceptional. They should be for failures around the edges. Not for controlling flows between expected and usual errors in your code!
There is a lot to think about here. Retries, Circuit-Breakers, Side-Cars, etc.
Your program will fail. Plan for it, test for it, monitor for it.
Understandable at runtime
Good code is observable
Earlier we spoke about predictability in the code. Can we understand the flows? How it all fits together? All that jazz.
This is not the only type of understandability we need.
We also need to understand what our program is doing under operation.
This is where observability comes in. It’s not really a property of how the code is written, but you will see it in the code. And if you read a program without any of it, I would hope you get a bad feeling about it.
Thanks to Open Telemetry, this one is easy nowadays. There are so many plug and play things. Just go for it. But don’t log too much, I’ve written about that one before
Where did all the good code go?
There is a sentiment out there that LLMs & Agentic Coding Tooling is destroying good code. The truth is that good code has always been relatively rare. I do not argue that an LLM is capable of pumping out some really bad code at astounding rates. But for one reason or another many developers have already been doing this for years.
It is important for now to not accept that LLMs mean code does not have to be good. This is a dangerous one.
In fact, you can use these new powerful Agentic Coding Tools to refactor larger codebases than you ever would have been able to before! As long as you have good tests that is. These tools are particularly good at this kind of work. LLMs should be making your code better than ever before. Not worse.
For decades Software Engineers have been told they are about to be put out of the job by a new invention. COBOL was even meant to do this back in the day! Business Analysts will be able to write the spec directly in COBOL and we don’t need those pesky expensive Engineers any more!
Well, fundamentally you need to describe your requirements in painful detail. Every edge case and interaction must be written up. And it must be done so in a structured way which cannot possibly be misinterpreted, and must be deterministic.
If you can do this, sure maybe you can feed this specification into the genie and have it produce a fully working program. Except what I have described is not necessarily prompting and LLMs. But rather Programming, and Compilers.
A fully robust specification is indistinguishable from programming.
I would argue that if you find you are having to create too much boilerplate and write application code that does not solve or describe problems that you care about as a part of your specification. You have chosen a programming language, or environment, at the wrong level of abstraction.
One mans treasure is another mans trash
Everything is subjective. I have explained to you the properties which I find “good code” has. You may agree, or disagree. Even if you agree with every property I have listed and how I have defined it. If we actually sat down and looked at some code, we would likely have some very different gut reactions, and even more different reasons for why we felt that way.
Good code produces the right value for the business at the right time. Nothing more, nothing less. The most artisanal beautiful scripture is worthless if it doesn’t do the right thing for the business. Unfortunately this makes the most ugly awful code, good code if it solved the right problem for the business at the right time.
Code which was good for a 20 person team will look very different to code which is good for a 100 person team. Do you work in a highly regulated environment? Or your startup is on a runway? Good will look very different to you. Try to take some of these principles away, but don’t be too rigid.