◀ prevnext ▶

The Good Code Manifesto

Programming · Nov 18th, 2025

0. The two rules

Every advice in this manifesto is based on two rules.

Rule 1: Good code is understandable.

Sooner or later you will realize that code is more often read than written. May your code be read by strangers on the internet, your coworkers, or you yourself in a couple of weeks, months or years. When requirements change, new features need to be added, or bugs to be fixed, you need to go read and understand the actual code. You can’t do your job if you can’t understand the code.

If the code you are writing is not understandable, it is not good code.

Rule 2: Good code works.

Whether you like it or not, you will eventually break rule 1. May that be because of the complexity of the problem, the API of a library you are using, or simply lack of ability, you WILL write code that is difficult to understand. In these cases, it’s preferable to get things working.

I’ve observed many instances in my career, where despite a working understandable prototype exists, someone insisted on a specific programming pattern, which then caused problems in the short and longterm. I’ve even seen systems break because of hastily applied programming patterns.

Rule 2 always holds. Use solutions that you are familiar with, and that you know are working. Don’t mindlessly follow what a book, a blogpost (including this one) or an AI said. My ego isn’t big enough to claim that my advice will work in every case. You will certainly find exceptions. My perspective comes mainly from graphics programming. I value readability, reliability and performance over everything else.

1. Use strong naming

If you have the choice, always pick the programming language with statically strong typing. By that I mean, choose languages that have to be compiled, and which spit out errors if you use types incorrectly. The stronger and stricter the type system, the better. While beginners and Python devs often see a strong type system as a major annoyance, it will always pay off in the long term. Always. The compiler is a tool, which can check the correctness of your program before it even runs. This reduces mental overhead and prevents bugs.

But no matter how strong your type system is, there will always be ambiguity. Take the humble int for example. The int alone can have many different meanings according to the context. These usages include but are not limited to:

Another favorite example of mine is the file path. Many operating systems use strings for paths. Rust in particular decided to have a seperate type for paths, to differentiate them from strings. But ambiguity still exists:

Sure, these can be checked at runtime, but the compiler is not preventing you to pass a filename to a function that expects a directory.

No matter how strong your type system is, there will be ambiguity. To reduce this ambiguity you should use strong naming. By that I mean, your variables, functions, structs, and everything else that can have a name, should have a proper, describable name. The name of a thing should clearly tell the reader what that thing is doing.

1.1 Variables

1.1.1 Avoid single letter names

First things first, single letter names are almost always discouraged. There are exactly 2 notable exceptions: Mathematical formulas and small scoped variables.

If you implement functions from a mathematical text, a book or online reference, sure, use the same names as used in the reference.

If you have variables that exist in a very small scope, single letter names are fine as well. These are your i, j, k variables in for-loops, or x in an iterator callback. For example

// this is fine
for (int i = 0; i < 100; ++i) {
    do_something_with(i);
}

or

// this is also fine
my_objects.foreach(x => x.do_something())

These instances of single letter names are acceptable, because their scope is small enough to easily reason about what that variable holds and does.

Other than these two exceptions, your variable should never have a one letter name. Instead, the name of a variable should be a full word or a combination of words that clearly indicate what is being stored and how it is going to be used.

1.1.2 Be careful with abbreviations

Names should be descriptive full words. Abbreviations are discouraged, but often okay, if the abbreviation is used very often and is clearly understood. For examble Vec3 for Vector3 is acceptable, because in a graphics programming environment this type will be found everywhere in the codebase. Or when you are frequently dealing with a file system, using dir instead of directory may also be acceptable. But always keep in mind that a new programmer looking at your codebase may not be familiar with the abbreviation.

1.1.3 Use pre- and posfixing

Let’s go back at the int example. If it is used as an index, postfix it with index:

int apple_index = get_apple_index();
Apple apple = apples[apple_index];

If you are using it as a counter, postfix it with counter. If you use it as a score, postfix it with score. And the same for the other examples.

Postfixing is also helpful in the filepath example: Let’s say you have a path to a configuration file. It’s absolute path would be stored in a variable called configuration_filepath, it’s name configuration_filename and it’s directory configuration_directory. So if you see a function signature like this:

Configuration[] get_all_configurations(string directory)

You know that the function requires a path that points to a directory, not a file.

You may notice that this kinda resembles Hungarian notation, but more verbose, and that’s exactly the idea. For the uninitiated, Hungarian notation was a naming convention to indicate how a given variable is to be used. It fell out of fashion, because people were misusing it. In the past, people denoted every variable with it’s type. For example every string must start or end with s and every int with i. If your programming language uses a statically strong typesystem, this is redundant and ultimately useless. The type system already denotes what type a variable is. So people were rightfully annoyed when code guidelines forced them to use this everywhere.

But I want to highlight that postfixing is exactly useful when the typesystem is not sufficient. Whenever ambiguity arises, postfixing is very valuable, as demonstrated in the paragraphs above and the examples to come. So let’s continue.

1.1.4 Be explicit about units

You should be explicit when using units.

When dealing with time, prefer a dedicated type. For example TimeSpan in C# allows you to construct a time interval from seconds, minutes and so on. If your programming language doesn’t have types like this, or you decide to store a time interval in a float, denote it with it’s unit. send_interval for example is nondescriptive, but send_interval_in_seconds clearly denotes how long a single sending interval is.

Often times postfixing units can be annoying, when you deal with many variables. When you consistently use the same unit everywhere, for example meters for distances, then you can omit them in your variable names. But in that case I encourage you to write a comment on declaration, what kind of unit this variable holds.

1.1.5 3d math

Postfixing is also helpful in 3d math.

In mathematics, there is a difference between a point and a direction. Both can be expressed as a vector, an array of floats. Thus many math libraries only implement the vector, with no differentiation between points and directions. This leads to confusion. Take for example the following function:

Vector3 rotate(Vector3 value, Rotation rotation);

value is nondiscriptive. Mathematically, a direction can be rotated, but rotating points is nonsensical. And as the function stands now, the compiler does not hinder you to pass a point into it. I usually postfix directions with direction or dir, while points with point, position or pos.

Essential to 3d math are rotations. Rotations are finicky. My advice here is to go learn what quaternions are and prefer them over everything else. Euler angles are common, especially in user facing UI, but they are deceptively counterintuitive. I’ve experienced so many difficult rotation problems directly caused by euler angles, that today I avoid them at all costs.

Often you want to rotate around a single axis anyway. Use an angle axis rotation in that case. Also refrain from using x, y or z in your variable names, like angle_x or y_rotation. When rotating around a single axis, use pitch, yaw and roll instead.

picture of pitch yaw and roll

When working with coordinate systems, often you are faced with wildly different ones. Most commonly you will deal with local vs world space, but there are so many more, especially between libraries that don’t know each other. The only standard is that there exists no standard. Y may point up, or it may point down, sometimes Z is up. Sometimes coordinate systems are left handed, sometimes they are right handed. Pretty much every direction for X, Y and Z is fair game.

When dealing with directions, refrain from using x, y or z as well. If you have a vector that points towards "X", chances are that X is not where you think it is. Rather use relative directions like left, right, up, down, forward and backward and combine it with the object. For example camera_right is the right direction relative to the camera, car_up is the up direction relative to the car, and world_forward is the forward direction relative to the world coordinate system.

1.1.6 Tangent: Magic values

Magic values are plain values with no variables or names associated with them.

Timestamp _time_since_last_bump = unix_epoch();

void update() {
    Timestamp now = get_now();
    Timespan diff = now - _time_since_last_bump
    
    // bad
    if (diff < 42) {
        return;
    }
    
    _time_since_last_bump = now;
    bump()
}

Here, 42 comes out of nowhere and gives no explanation on why it is 42. This is especially egregious, as per 1.1.4, we don’t even know what units we are using. A better solution is to provide a variable, local or global, and store your value there:

float _bump_interval_in_seconds = 42; // magic value is now a variable
Timestamp _time_since_last_bump = unix_epoch();

void update() {
    Timestamp now = get_now();
    Timespan diff = now - _time_since_last_bump
    
    // good
    if (diff < _bump_interval_in_seconds) {
        return;
    }
    
    _time_since_last_bump = now;
    bump()
}

This is better, because as a variable, its name now indicates what the value actually represents.

Now, I did cheat a bit with this example, as a proper time library shouldn’t allow comparison between a Timespan and a number, but you get the point.

1.2 Functions

In different programming languages, functions are referred to by different names. Functions, methods, lambdas, closures, procedures, subroutines or whatever. Semantically they are all the same, they just differ in scope and access to their surroundings. When I talk about “functions”, I mean any callable code. Functions are asociated with a block of code, which is executed when the function is called. As such, a function does stuff, and thus its name should be a verb. Like with strongly named variables, strongly named functions should have descriptive full word names.

1.2.1 Denote dangerous functions

Depending on how the function is implemented, its name must be more specific. For example, functions can be asynchronous. From the perspektive of the client, asynchronous functions return immediately, but its code block is executed in parallel, finishing in an unspecified point in the future. Postfix such functions with async.

Async functions should return an object that allows client code to wait on the result, may that be a Task, Future, Promise or whatever.

Future send_message_async(string message) // good
void send_message_async(string message)   // bad, not awaitable
Future send_message(string message)       // bad, not named async

Many modern programming languages come with syntax sugar to aid the programmer in generating awaitable objects. But if your programming language can’t provide such an object, implement a blocking alternative to your function. Ideally, client code should be in charge whether a given operation blocks or not.

Also, recursive functions should be postfixed with recursive:

GameObject find_game_object_recursive(GameObject game_object, Id id) {
    if (game_object.id() == id) {
        return game_object;
    }

    foreach (GameObject child in game_object.children()) {
        GameObject result = find_game_object_recursive(child, id); // recursive call
        if (result != null) {
            return result;
        }
    }

    return null;
}

If your data is cyclical, or your exit condition is never met, then recursive functions will loop forever. Hopefully you will overflow the callstack and crash. Crashing is good, because it obviously tells you that something is wrong. But in bad cases you loop forever. This is almost always undesireable. Sometimes, recursive functions are not obviously recursive. For example function A may call function B, which calls function C, which calls function A. By postfixing it with recursive, it makes recursion easier to spot and tells the reader of your code, they must take care when handling them.

Recursion is so problematic in fact, that avoiding recursion is rule number one in “The Power of 10: Rules for Developing Safety-Critical Code” by NASA. Assuming you are not writing code for satalites or rovers, recursion is fine and a useful tool. Still, you should label it clearly when using it.

1.2.2 Out parameters

Some functions have out-parameters, which are usually pointers to be written to. Languages like C# even have a specific keyword for that. In cases where you don’t have a keyword for out-parameters, it’s a good idea to prefix out parameters as such:

bool try_create_file(string filepath, File* out_file)

1.2.3 Mark failable operations

The function in the example of 1.2.2 is what I call a “try-function”. Functions can fail. And failable functions should be clearly annotated in some way. Languages like Rust allow failure states within it’s typesystem, via the Option and Result types. But in languages where this is not possible, prefer to use try-functions.

A try-function does not return it’s result, instead it returns a bool, int or an enum, to indicate if the operation was successful. The actual return value is returned via an out parameter. If the operation failed, the out parameter may not be initialized and must not be used.

Also, as rule 7 of “The Power of 10: Rules for Developing Safety-Critical Code” by NASA states, return values must always be checked, or explicitly discarded. This is especially true for try-functions, as a failed operation can break the surrounding code if not handled properly.

You would use a try-function like this:

File file;
if (try_create_file("some path", &file)) {
    // success
} else {
    // failure
}

1.2.4 Avoid default parameters

Some programming languages allow default parameters in functions. A default parameter declares a constant value, which then can be omitted by the caller.

// bad
void draw_aabb(Vec3 min, Vec3 max, Rgb color = null);

void main() {
    // the third parameter was omitted
    draw_aabb(Vec3::zero, Vec3::one);
}

This is always bad design, because being explicit is better than being implicit. With default parameters you encourage client code to avoid passing arguments into the function, leading to hidden dependencies.

Such hidden dependencies often break when refactored. Client code may have made assumptions that after refactoring don’t hold true anymore. This introduces bugs that a compiler may have cought. Also, if client code did pass a value, and a parameter may be removed due to refactoring, chances are that now the client code passes the value into a different parameter. The compiler may not catch this, which again introduces a bug. These kinds of issues are especially true when using implicit type coercion.

By avoiding default parameters like this, you avoid these refactoring headaches and end up with more stable code. If you still absolutely need default parameters, see 1.2.5.

1.2.5 Use structs when dealing with many parameters

If your function is complicated enough, it may bloat to ten or more arguments. In this case, consider declaring a struct that holds all parameters and pass that as an argument. This drastically reduces the amount of parameters of your function, which improves readability.

As an example I give you the VkGraphicsPipelineCreateInfo structure from the Vulkan graphics API, which is passed into the vkCreateGraphicsPipelines function:

typedef struct VkGraphicsPipelineCreateInfo {
    VkStructureType                                  sType;
    const void*                                      pNext;
    VkPipelineCreateFlags                            flags;
    uint32_t                                         stageCount;
    const VkPipelineShaderStageCreateInfo*           pStages;
    const VkPipelineVertexInputStateCreateInfo*      pVertexInputState;
    const VkPipelineInputAssemblyStateCreateInfo*    pInputAssemblyState;
    const VkPipelineTessellationStateCreateInfo*     pTessellationState;
    const VkPipelineViewportStateCreateInfo*         pViewportState;
    const VkPipelineRasterizationStateCreateInfo*    pRasterizationState;
    const VkPipelineMultisampleStateCreateInfo*      pMultisampleState;
    const VkPipelineDepthStencilStateCreateInfo*     pDepthStencilState;
    const VkPipelineColorBlendStateCreateInfo*       pColorBlendState;
    const VkPipelineDynamicStateCreateInfo*          pDynamicState;
    VkPipelineLayout                                 layout;
    VkRenderPass                                     renderPass;
    uint32_t                                         subpass;
    VkPipeline                                       basePipelineHandle;
    int32_t                                          basePipelineIndex;
} VkGraphicsPipelineCreateInfo;

VkResult vkCreateGraphicsPipelines(
    VkDevice                                    device,
    VkPipelineCache                             pipelineCache,
    uint32_t                                    createInfoCount,
    const VkGraphicsPipelineCreateInfo*         pCreateInfos,
    const VkAllocationCallbacks*                pAllocator,
    VkPipeline*                                 pPipelines);

These “CreateInfo”, or “Args” structs can hold a ludicrous amount of information, without losing organization. A function with comparably many parameters are difficult to call, because parameter 6 and 7 are difficult to distinguish, leading you to count them each and every time.

Also, args structs may or may not have default initialization, which isn’t a problem here, because each field has a name. There is no ambiguity on constructing such an object, especially in languages like Rust where everything must be initialized.

An args struct can also be built step by step. Then, when the preparations are done, you can pass the arguments elegantly in a single call.

1.2.6 Avoid side effects

A function produces a side effect, if it modifies something outside of its declared signature. For example a function foo() may change a global variable, or a function of a class modifies a dependency that the class stores internally.

Side effects are bad, because client code may not be aware of them. This produces implicit dependencies. I repeat myself, but being explicit is better than being implicit. If the dependency changes, now client code breaks.

Thus, implicit dependencies tend to produce bugs and they are often difficult to debug.

1.3 Structs and classes

Structs and classes store data. As such their names should be nouns. As should be clear by now, their names should consist of descriptive whole words.

Now when it comes to classes, many programmers are really tempted to use undiscriptive names. Names like InputManager or InputHandler are a common occurance in the wild. The issue: Every piece of code manages and handles data. That’s a hard fact. As such, names like “Manager” or “Handler” are absolutely undiscriptive and meaningless.

Sticking with the input example, you should name it depending on what your input system does and how it is implemented. For example, if it is run in a game engine which has a main loop, name it InputCollector, as it collects the input and produces an InputState object that is used later in the loop. Or name it InputMapper when it maps buttons to actions like “Jump” or “Walk”. Or maybe your program is an event based GUI application, in which case InputEventProducer or InputEventInvoker is more fitting.

Also order your fields. Alphabetically, semantically or both. Makes them easier to find when you have a lot of them.

1.4 Conditions

When it comes to conditions, mostly used in if statements, rules 1.1 to 1.3 are somewhat unfitting. Thus I dedicated an entire section just to conditions.

1.4.1 Use strong naming

Variables and functions used in an if statement should resemble questions that are answerable with “yes” or “no”. Here is a bad example:

// bad
if (check_input(input)) {
    ...
}

Even though check_input satisfies the advice in previous sections, the code above is bad. Yes, “checking” is what it does, but what is it checking? Whether the input is valid? Maybe if the input object has missing fields that must be added, or does it determine whether a button was pressed? The code does not tell you anything. Better code would look like this:

// good
if (input.is_valid()) {
    ...
}

or

// good
if (b_button.is_pressed()) {
    ...
}

If your programming language doesn’t support the . operator, the same can easily be accomplished with variables:

bool input_is_valid = validate_input(input);
if (input_is_valid) {
    ...
}

or

bool b_button_is_pressed = button_is_pressed(b_button);
if (b_button_is_pressed) {
    ...
}

1.4.2 Avoid negative names

Negative names for variables and functions are bad and should be avoided. By negative names I mean names that include words like “no”, “not”, “isnt”, “doesnt”, “wasnt”, “cannot” and so forth. For example:

// very bad
if (input.is_not_valid()) {
    ...
}

Eventually you need to negate statements. Maybe not now, but maybe in the future. If you negate such a statement you get:

// yikes
if (!input.is_not_valid()) {
    ...
}

Double negative. This is logically sound, but it is difficult to process for our human brains. Instead, always stick with positive names, even if you need to immediately negate them:

bool input_is_valid = validate_input(input);
if (!input_is_valid) {
    ...
}

Negation is pretty much free, especially in compiled languages.

1.4.3 Avoid complicated conditions

// oh god
if (input.is_valid() && !ctrl_button.is_pressed() && !shift_button.is_pressed() && (a_button.is_pressed() || b_button.is_pressed())) {
    character.jump();
}

If you are using a compiled programming language, variables are free. Even scripting languages like JavaScript and Python come with compiled runtimes nowadays. The compiler optimizes variables out if they are not needed. So I encourage you to avoid one liners (like the condition above) and use as many variables as possible (like the code below). The code below builds the condition step by step, which greatly improves readability.

bool input_is_valid = input.is_valid();
bool modifier_botton_is_pressed = ctrl_button.is_pressed() || shift_button.is_pressed();
bool can_jump = input_is_valid && !modifier_botton_is_pressed;
bool jump_button_pressed = a_button.is_pressed() || b_button.is_pressed();

if (can_jump && jump_button_pressed) {
    character.jump();
}

Code like this comes with a small caveat however: Lazy evaluation. For example the buttons may have a hidden dependency on input. If the input is invalid, ctrl_button.is_pressed() may throw an exception. The complicated conditions from the previous example does not run into this issue, because && is lazily evaluated. When input.is_valid() returns false, ctrl_button.is_pressed() is not called. Issues like these are handled on a case by case basis. But when no lazy evaluation is necessary, many variables are preferable.

Besides, hidden dependencies are discouraged. See 1.2.6.

2. Formatting

2.1 Style

Since I am telling you how you should name your code, I should also tell you how to format it. Yes, I am talking about the dreaded tabs vs spaces debate. snake_case vs CamelCase. Should { go in the same line of the function declaration, or should it go in the line below? There is only one objectively correct answer. And you can quote me on that:

It doesn’t matter.

The only thing that DOES matter is consistency. Whatever style you choose, you should enforce it everywhere. If you and your team adhere to the principles of this manifesto, bad code will stick out because it will simply look different. But if your formatting is all over the place, then it’s more difficult to spot bad code. Consistent formatting makes it easier for yourself and your team.

I recommend you get in the habit of using an automatic code formatter, and run it frequently.

2.2 Attempt to decrease indentation

A new scope is commonly indented:

// global scope

int main() {

    // outer local scope

    {

        // inner local scope

    }
}

But too much indentation is bad. Whitespaces denote little to no information, and too much indentation leaves your screen blank. When the indentation becomes bad enough, the code may exceed the width of your editor. Some text editors wrap lines; some go offscreen, forcing you to scroll horizontally. Both these options suck when trying to navigate code.

To avoid indentation, early returns are your friend. Instead of this:

// bad
void do_stuff() {
    if (condition_is_true) {
        ...
    }
}

you can do this:

// good
void do_stuff() {
    if (!condition_is_true) {
        return;
    }

    ...
}

Notice that because of the early return, the ... (potentially many many lines of code) has one less level of indentation.

Early returns can also be used in loops, but you would use continue instead. Instead of this:

// bad
for (int i = 0; i < 100; ++i) {
    if (can_do_something_with(i)) {
        ...
    }
}

you can do this:

// good
for (int i = 0; i < 100; ++i) {
    if (!can_do_something_with(i)) {
        continue;
    }

    ...
}

Each early return reduces the indentation level by one, freeing up your screen to actually display code instead of whitespaces.

2.3 Avoid wide code

Indentation can lead to your code to exceed the width of your editor, but so can wide code. This includes functions with a lot of parameters or the builder pattern found in Rust or LINQ in C#. In these cases, prefer to split your code into multiple lines of code:

// bad
CreateWindow(param1, param2, param3, param4, param5, param6, param7, param8, param9);
Fruit[] red_fruit = fruit_list.Where(fruit => fruit.is_ripe()).Select(fruit => fruit.color() == Color.Red).ToArray();

// good
CreateWindow(
    param1,
    param2,
    param3,
    param4,
    param5,
    param6,
    param7,
    param8,
    param9
);
Fruit[] red_fruit = fruit_list
    .Where(fruit => fruit.is_ripe())
    .Select(fruit => fruit.color() == Color.Red)
    .ToArray();

Too many function parameters are discouraged anyway. Follow the advice in 1.2.5 to reduce the number of your function parameters.

Chances are that the bad examples above are rendered poorly, forcing you to scroll horizontally. This demonstrates my point that wide code is bad.

Also important to note, is that code is read in many different applications: IDEs, text editors, in your browser, merge tools and other side by side views on a single screen. As such the width of the actual view into your code is smaller than you might expect. Some modern IDEs provide you with a vertical line and I heavily encourage everyone to not overstep that line.

screenshot of rider

2.4 Keep branches close to each other

If you have an if-else construct, consider negating the condition, if that puts the smaller branch ontop of the other:

// bad
if (condition_is_true) {
    do_operation_a();
    do_operation_b();
    do_operation_c();
} else {
    do_operation_z();
}

// good
if (!condition_is_true) {
    do_operation_z();
} else {
    do_operation_a();
    do_operation_b();
    do_operation_c();
}

Doing so puts the else closer to the if. This makes it easier to find. Thus control flow is easier to determine and follow.

3. Comments

Comments are equally decisive as formatting. They are hotly debated in online communities. Their main problem stems from the fact that they are not checked by the compiler, and thus are by definition dead code.

Comments make it very easy to temporarily remove code from execution. But this inherently means any information in the comment may or may not be true. Commented code may not compile when commented back in. The code may have been correct when it was written, but since then things have changed, making the comment untrue in the process.

From my experience there are 4 valid reasons to use comments. If a comment does not fulfill any of the following 4 reasons, it’s a bad comment and must be deleted.

3.1 Comments to quickly remove code from execution without deleting it

Removing code without deleting it is helpful in prototyping. But once you are done with your prototype and want to ship this code, you should remove ALL outcommented code. No one knows how long this code will stay commented out and it may age very badly.

In rare cases you can keep outcommented code, when it shows some implementation that was difficult to figure out, but isn’t needed right now. But other than these rare occations, commented code should be removed.

3.2 Comments to aid in structure

Inside your class you can have sections that denote different things.

class FruitCollection {
    // Constants
    const int THE_ANSWER = 42;

    // Members
    List<Fruit> _fruit;

    // Constructor
    public FruitCollection() {
        ...
    }

    // Public Methods
    public void AddApple(Apple apple) {
        ...
    }

    public List<Fruit> GetAllApples() {
        RemoveBadFruits();
        return _fruit.copy();
    }

    // Private Method
    private void RemoveBadFruits() {
        ...
    }
}

Here, comments function as headers, or section titles. Proper structure and organization into regions makes it much easier to navigate your code, especially when your file begins to bloat to hundreds and thousands lines of code. When all your teams classes/functions look like that, you will find that you can easily navigate your code, even code which has been written by other people.

Modern IDEs come with pragma, region or section statements that help you structure your code. But if your programming language or IDE does not support them, comments can do that job just as well.

Comments also help with structuring large functions:

int main() {
    // start system timer
    Timer timer = Timer::start();

    // get cli args
    string[] raw_args = get_cli_args();

    // parse cli args
    Args args = parse_args(raw_args);

    // execute program
    run(args)

    // print duration
    TimeSpan elapsed = timer.elapsed();
    println("program finished in " + elapsed);
}

To keep the example brief, the code between each comment is just a single line. But when individual steps take 10-20 lines of code, comments like this provide a great visual anchor.

Tangentially related: Prefer to structure blocks of code depending on what they do. Instead of writing a wall of text:

// bad
do_operation_a();
do_operation_b();
do_operation_c();
do_operation_x();
do_operation_y();
do_operation_z();

do this:

// good
do_operation_a();
do_operation_b();
do_operation_c();

do_operation_x();
do_operation_y();
do_operation_z();

Sometimes it helps to squint your eyes and look at your code with blurry vision. If your code fades into a massive blob, chances are that it’s difficult to read. Thus, try to divide your code into discrete blocks. A lone newline here and there can make your code much more readable.

3.3 Comments as documentation

One reason to use comments is documentation, in the form of documentation comments. Syntax differs between programming languages and doc systems, but in most cases you put a specially formatted comment above a struct, class or function. This comment describes what the given code does, which may be displayed in your IDE or may be extracted into a separate document by an external tool.

From my experience, good documentation takes A LOT of time and effort. Chances are you are underestimating how much work goes into good documentation, so let me elaborate.

If you write documentation comments, it falls victim to the problem I outlined at the start of this section: If behaviour changes, then the documentation is outdated and potentially untrue. As such, proper documentation must be maintained, just like the code they describe. And maintenance costs time and money.

If the API implementation is private, for example a C API just provides a headerfile with no source code, documentation makes sense. Documentation also makes sense if the library you are writing is intended to be used by external people, i.e. strangers on the internet, customers or members of another team. But if the code is public and accessible, any programmer can just look at the source code and simply read what it does.

3.4 Comments to explain confusing code

Let me remind you that that you will eventually break rule 1: “Good code is understandable”. Rule 2: “Good code works” always holds. You should write comments for code that breaks rule 1.

With comments you can explain why the code is the way it is.

Here’s some code, that is inspired by a real implementation of some node generation code I have once worked with:

List<Node> Deserialize(Data toDeserialize) {
    var callbacks = new List<Action>();
    var nodes = new List<Node>();

    BuildNodesRecursive(toDeserialize, callbacks, nodes);

    var mainThreadTask = _mainThreadDispatcher.Enqueue(() => {
        foreach(var callback in callbacks) {
            callback();
        }
    });

    mainThreadTask.Wait();
    return nodes;
}

void BuildNodesRecursive(Data data, List<Action> callbacks, List<Node> nodes) {

    // `BuildNodeFromData` must be run on the main thread.
    // however, benchmarking proved that `BuildNodesRecursive`
    // is called very often, which leads to contention in the
    // dispatcher. simply the act of enqueuing many jobs leads
    // to a performance decrease. to increase performance, we
    // collect the actions instead, and enqueue them only once
    var buildNodeCallback = new Action(() => {
        var node = BuildNodeFromData(data);
        nodes.Add(node);
    });
    callbacks.Add(buildNodeCallback);

    var children = FindChildren(data);
    foreach (Data child in children) {
        BuildNodesRecursive(child, callbacks, nodes);
    }
}

A specific function must be run on the main thread. So why wouldn’t we just enqueue it immediately? Why go through the hassle to allocate a new List and hand them through the functions? Well, the comment tells you why.

Also note that such comments require discipline from you as a programmer. If you modify code and find a comment nearby, you must evaluate the comment. If it isn’t valid, you must correct it. If it isn’t necessary anymore, you must remove it.

If you don’t remove invalid comments, then you have an invalid comment in your codebase. Invalid comments will confuse the next programmer that comes across it.

4. Miscellaneous

4.1 Inheritance is evil

The OOP vs functional programming debate has been beaten to death. My stance on it is that both have their strengths. Good programming languages are multi paradigm and combine the best of both worlds.

But inheritance in particular is bad, and I discourage everyone from using it.

Inheritance has a single good usecase: Polymorphism. If you require polymorphism, having a single base class/interface with no implementation is fine. But other than that, inheritance is to be avoided at all costs. Use composition instead.

// bad
class Base {
    void say_hello();
}

class Child : Base {
    void say_hello() {
        base.say_hello();
    }
}

// good
class Base {
    void say_hello();
}

class Child {
    Base _base;
    
    void say_hello() {
        _base.say_hello();
    }
}

This goes back to the idea that being explicit is better than implicit.

Inheritance allows a lot of implicit behaviour, with keywords allowing inheriting code to overwrite methods, hide or ignore them. When you have a given type and call a method on it, it isn’t inherintly clear what method of what type is now being called. If you have a Child stored in a Base variable and call say_hello() on it, what method will be called? Depending on the syntax and keywords, and your programming language of choice, results may vary wildly.

With composition, you make classes reference each other, instead of allowing one to be used in place of another. This completely removes any ambiguity. Calling say_hello() on Base will call the method on Base. Calling it on Child will call it on Child. How Child decides to route the call to Base, if at all, is clearly stated in the method body.

4.2 Encapsulate behaviour, not structure

One of the most damaging “clean code” advice I see floating around the web and in my office is this: Files shouldn’t have more than 100 lines of code, and functions not more than 10 lines of code. The actual numbers change from source to source, but the idea is that a piece of code shouldn’t be too long. This advice is bullshit and results in unreadable garbage.

If the problem is complex enough, you will write a lot of code. Any form of encapsulation is scattering your code. In very bad cases you scatter your code over multiple files.

Here’s why this is a problem: When you go back and try to understand how things work, you now need to take multiple files into account. You require a mental map, which drastically increases the mental overhead required to debug it.

Contrast that to a single big function that runs from top to bottom. You know a code block on top runs before a code block below. The call order is obvious. When the code is scattered, the call order is not that obvious.

A good rule of thumb: If your function is used only a single time, and it is private (meaning it is only accessible in the same class/module/file were it is defined), it’s probably a good idea to inline it. Follow the points described in 3.2 to properly structure big functions.

Encapsulation should not be used because of structural reasons. Encapsulation should be used because of behavioural reasons.

A good example for encapsulation is code reuse. If you have a piece of code that is used more than once, you probably want to encapsulate it in a dedicated function and call that, even if it is private.

Another good example is a public facing function. For example an asset compiler may recursively iterate through a directory, copy each file, assign each file an id, resolve the ids internally and write them all into a single file. Client code shouldn’t care what these steps are or how these work. All the client code cares about is that if it calls compile(), it does what it’s name is suggesting it does.

4.3 Fix warnings and lints

Warnings try to make you aware that you are doing something incorrectly. Yes, technically it compiles, and technically your code appears to work, but in the edgecase it will break. As such, warnings are to be taken seriously and they should be fixed. If your programming language allows to disable warnings, NEVER do so. If your programming language allows to enable even more pedantic warnings, always do so, as rule 10 of “The Power of 10: Rules for Developing Safety-Critical Code” by NASA states.

Fixing your warnings does not only make your code more stable, it also helps to keep your development environment clean. Let me elaborate. Whether you work with a terminal or IDE with a GUI, warnings and lints attempt to catch your attention in one way or another.

For example you use a compiler in the terminal and your code doesn’t compile. In that case it can take extra time to find and read the error, when it is buried under hundreds of warnings.

In another example, when working with a GUI IDE, selecting a word usually highlights all occurances of that word. This makes it easier to find their usages in the same file. If your IDE is really fancy it even highlights your scrollbar, as some sort of minimap, which allows you to get some spatial idea of your file. However, if this view is cluttered with warnings, features similar to this become unusable. And even if your IDE GUI manages to seperate hints and warnings, they still use valueable screenspace calling attention to themselves. This may hinder your concentration.

Lints are not as problematic, but they hint at a better coding style. When implemented, they often improve readibility and structure. Chances are that you will come across some of the points I have discussed in this blogpost as lints thrown by your linter. Depending on the programming language and linter, lints can be somewhat subjective at times, but it’s always good practice to consider their usefulness. If they are useful, implement them. If they are not, manually disable the lint.