LogoNextJet
All Articles
Writing Clean Code That Lasts
9 min read

Writing Clean Code That Lasts

Writing Clean Code That Lasts

Writing Clean Code That Lasts

A

Renas Hassan

Apr 30th, 2025

I've worked with numerous codebases at different companies, and I've seen both clean and messy code. The costs of writing bad code might not be obvious at first. On the surface, it looks like we're shipping features quickly, but everything has a cost. Eventually, it will bite you, and it's never pretty.

The cost of bad code

Let's exaggerate a bit to see how fast the dominoes fall when nobody cares about code quality.

Let's draw a scenario where you work in a company that lacks guidelines or strict boundaries for the code you're shipping. Code quality is just an afterthought, and everybody is just writing code as they please. Not to mention, upper management is also pushing you with tight deadlines. With every commit to the main branch, you're introducing more and more technical debt. With every new feature, you think to yourself:

Eh, no big deal, I'll refactor this later.

But let's be honest, you'll never refactor that code. You'll be too busy shipping new features. Then the next time someone visits that code, they'll have to spend hours trying to understand it.

They'll find themselves in a situation where they'll be like:

Why is this so complicated?

WTF?

Am I stupid?

They'll face a difficult choice: either invest time in refactoring the existing code or build on top of the shaky foundation. Since they're under pressure to ship, they'll most likely choose the latter. It's harder to write clean code when you're building on top of already bad code, the surrounding chaos will inevitably spill over.

This creates a vicious cycle of bad code, and that's how technical debt compounds. Eventually, the codebase will become so bad that basic features take five times longer to implement. With every new feature, more and more bugs are introduced due to the poor code quality. Now you're having a harder time competing with competitors who are able to ship features way faster.

Eventually, this frustration often leads engineers to reach their breaking point and leave the company. This creates a new challenge as the organization must now hire replacements who lack both familiarity with the codebase and the historical context that the previous team possessed. The new engineers have a harder time getting up to speed, which slows down progress and keeps the technical debt growing.

Eventually, the only logical thing is to do a complete rewrite of the codebase and you're back to square one.

The advantages of clean code

The above is just one example of how bad code can impact a company. Let's try to understand the benefits of writing clean code.

As software engineers, we are going to be reading code A LOT more than we are going to be writing it. It would be best to be able to read and understand the code without having to spend hours trying to understand it due to some poorly written code.

Beyond the time it saves you during development, clean code also offers several practical advantages:

It's easier to:

  • maintain
  • extend
  • test
  • debug
  • refactor
  • collaborate

Don't believe me? Here's how your PR comments start to look like:

llama_dev's avatar
llama_dev commented 10 minutes ago
Member

Absolute giga chad code, no need for tests, just ship it!

monkey_bznz's avatar
monkey_bznz commented 1 hour ago
Member

Please teach me senpai

morty-hype-man's avatar
morty-hype-man commented 3 hours ago
Member

Just push to main at this point, no need for PRs 🤩

Clean code in practice

I'm not going to go through all the principles of clean code in this article, because there are just too many for one article.

Recommendation

If you wish to dive deeper, I recommend reading Clean Code by Robert C. Martin.

However, I do want to give you some quick and impactful clean code practices you can apply today that will give a major boost to your code quality.

Let's start off with a piece of code that is bad and see how we can improve it step by step by applying some of the principles of clean code.

The "bad" code

Here's a function that calculates the final price of an order. It applies a discount based on the user type (1 for regular, 2 for premium) and adds tax. It works, but it's hard to read and maintain.

order-processor.ts
function calculatePrice(items: any, userType: number) {
  let total = 0;
  for (let i = 0; i < items.length; i++) {
    // Calculate subtotal for each item
    total += items[i].price * items[i].qty;
  }

  // Apply discount
  if (userType === 1) { // Regular user
    total = total * 0.95; // 5% discount
  } else if (userType === 2) { // Premium user
    total = total * 0.85; // 15% discount
  }

  // Add tax
  total = total * 1.10; // 10% tax

  // Return the final price
  return total;
}

// Define the order items
const orderItems = [{ price: 10, qty: 2 }, { price: 25, qty: 1 }];
// Calculate the final price for a premium user
const finalPrice = calculatePrice(orderItems, 2); // Premium user
console.log(`Final price: ${finalPrice}`);

There are several issues with this code:

  • Excessive comments that explain what the code does.
  • Unclear variable names (items, userType, total).
  • Magic numbers (1, 2, 0.95, 0.85, 1.10).
  • Multiple responsibilities (calculating subtotal, applying discount, adding tax).
  • Weak type safety.

Remove comment noise

I quite commonly see codebases that are filled with comments that explain what the code does. This is merely a band-aid for unclear code. Instead, strive to write code that is self-explanatory. Comments add a lot of noise, require maintenance, and can become outdated.

So when should you use comments?

  • When you need to explain why you did something, such as a trade-off, architecture decision, or a business rule.
  • When you need to add a TODO or FIXME to remind yourself or others to come back to something.

See the difference for yourself, compare the noisy "bad code" above to the version below, which is much less noisy without explanatory comments:

order-processor.ts
function calculatePrice(items: any, userType: number) {
  let total = 0;
  for (let i = 0; i < items.length; i++) {
    total += items[i].price * items[i].qty;
  }

  if (userType === 1) {
    total = total * 0.95;
  } else if (userType === 2) {
    total = total * 0.85;
  }

  total = total * 1.10;

  return total;
}

const orderItems = [{ price: 10, qty: 2 }, { price: 25, qty: 1 }];
const finalPrice = calculatePrice(orderItems, 2);
console.log(`Final price: ${finalPrice}`);

Add meaningful names and types

The beauty of this principle is that it's so simple that it's easy to overlook. But don't sleep on it, it makes a huge difference. It's about giving names to variables, parameters, and functions that make sense and are descriptive.

So how do you write good names?

  • Reveal intent: The name should clearly state what the variable represents or does.
  • Be specific & descriptive: Avoid vague terms like data or temp. Use names like orderItems instead of just items.
  • Use relevant terms: Use terms that are specific to the domain or subject matter, such as discountRate or customerAddress.
  • Follow conventions: Prefix boolean variables with is or has e.g. isLoading, hasPermission.
  • Prioritize clarity: Choose longer, descriptive names instead of short, unclear abbreviations.
  • Distinguish meaningfully: Make sure names clearly differentiate variables with similar roles (e.g., subtotal, discountedTotal, finalTotal).
  • Be searchable: Avoid single letters or cryptic names that are hard to find in the codebase like i, x, y.
  • Avoid misleading names: Make sure variable names accurately reflect their content or purpose to prevent confusion.
order-processor.ts
type OrderItem = {
  price: number;
  quantity: number;
};

type UserType = 'regular' | 'premium';

function calculateOrderTotal(orderItems: OrderItem[], userType: UserType): number {
  let subtotal = 0;
  for (let i = 0; i < orderItems.length; i++) {
    subtotal += orderItems[i].price * orderItems[i].quantity;
  }

  if (userType === 'regular') {
    subtotal = subtotal * 0.95;
  } else if (userType === 'premium') {
    subtotal = subtotal * 0.85;
  }

  const finalTotal = subtotal * 1.10;

  return finalTotal;
}

const items: OrderItem[] = [{ price: 10, quantity: 2 }, { price: 25, quantity: 1 }];
const price = calculateOrderTotal(items, 'premium');
console.log(`Final price: ${price}`);

We didn't make any changes to the logic of the code, yet it is now much easier to understand and reads like english.

Here are the improvements we made:

  • Variable names: The improved variable names make the code self-documenting.
  • Order item type: We added a type for the order item. Only use the any type as a last resort. What is the point of using typescript if you're not going to use types?
  • User type: We changed the userType parameter from a number to a string literal. Having to mentally map a number to a user type is error prone and hard to understand. You constantly have to remind yourself that 1 is regular and 2 is premium. Now when you pass arguments to the function, you get the added benefit of autocompletion.

Eliminate magic numbers

Magic numbers are unnamed numerical constants in code that lack context or explanation. For example, seeing 0.95 in a calculation without any indication of its purpose can be confusing.

Trying to make sense of magic numbers and what they represent adds extra cognitive load and can send you down a rabbit hole of trying to figure out what a magic number represents.

Also, if you use the magic number in multiple places, you have to update it in every place you use it.

Fortunately, this problem has an easy fix: use named constants. Using named constants instead of hardcoded values for things like discount rates and tax multipliers clarifies the code's purpose and makes future changes easier.

The convention for naming constants in javascript/typescript codebases is to use all caps delimited by underscores.

order-processor.ts
type OrderItem = {
  price: number;
  quantity: number;
};

type UserType = 'regular' | 'premium';

const REGULAR_USER_DISCOUNT_RATE = 0.95 as const;
const PREMIUM_USER_DISCOUNT_RATE = 0.85 as const;
const TAX_RATE_MULTIPLIER = 1.10 as const;

function calculateOrderTotal(orderItems: OrderItem[], userType: UserType): number {
  let subtotal = 0;
  for (let i = 0; i < orderItems.length; i++) {
    subtotal += orderItems[i].price * orderItems[i].quantity;
  }

  if (userType === 'regular') {
    subtotal = subtotal * REGULAR_USER_DISCOUNT_RATE;
  } else if (userType === 'premium') {
    subtotal = subtotal * PREMIUM_USER_DISCOUNT_RATE;
  }

  const finalTotal = subtotal * TAX_RATE_MULTIPLIER;

  return finalTotal;
}

const items: OrderItem[] = [{ price: 10, quantity: 2 }, { price: 25, quantity: 1 }];
const price = calculateOrderTotal(items, 'premium');
console.log(`Final price: ${price}`);
Note

Using as const for REGULAR_USER_DISCOUNT_RATE, PREMIUM_USER_DISCOUNT_RATE, and TAX_RATE_MULTIPLIER ensures that these values are treated as exact literal numbers (0.95, 0.85, 1.10). This means they are not just any number, but the specific numbers you set.

If you use as const for objects or arrays, it will make them readonly. This prevents you from accidentally changing the values. For example, you will be prohibited from using the push method on an array or changing a property of an object.

Apply single responsibility principle (SRP)

This is hands down the most important principle of clean code.

Definition

A module, class, or function should have one, and only one, reason to change.

Robert C. Martin, author of "Clean Code"

Ok, so what does this actually mean? It's basically that a file, class, or function should do one thing and one thing only. It's about dividing responsibilities and making sure each function is focused on a single task.

For example:

  • A function that calculates the subtotal of an order.
  • A function that applies a discount to the subtotal.
  • A function that applies tax to the subtotal.

You wouldn't want to have a function that does all of these things (unless it's an orchestrator function).

Breaking the main function into smaller, focused functions (calculating subtotal, applying discount, applying tax) makes each part easier to understand, test, and reuse.

order-processor.ts
type OrderItem = {
  price: number;
  quantity: number;
};

type UserType = 'regular' | 'premium';

const REGULAR_USER_DISCOUNT_RATE = 0.95 as const;
const PREMIUM_USER_DISCOUNT_RATE = 0.85 as const;
const TAX_RATE_MULTIPLIER = 1.10 as const;

const DISCOUNT_RATES: Record<UserType, (amount: number) => number> = {
  regular: (amount) => amount * REGULAR_USER_DISCOUNT_RATE,
  premium: (amount) => amount * PREMIUM_USER_DISCOUNT_RATE,
} as const;

function calculateSubtotal(orderItems: OrderItem[]): number {
  return orderItems.reduce((acc, item) => acc + item.price * item.quantity, 0);
}

function applyDiscount(amount: number, userType: UserType): number {
  return DISCOUNT_RATES[userType](amount);
}

function applyTax(amount: number): number {
  return amount * TAX_RATE_MULTIPLIER;
}

function calculateOrderTotal(orderItems: OrderItem[], userType: UserType): number {
  const subtotal = calculateSubtotal(orderItems);
  const discountedTotal = applyDiscount(subtotal, userType);
  const finalTotal = applyTax(discountedTotal);
  return finalTotal;
}

const items: OrderItem[] = [{ price: 10, quantity: 2 }, { price: 25, quantity: 1 }];
const price = calculateOrderTotal(items, 'premium');
console.log(`Final price: ${price}`);

Just take a moment to appreciate how much cleaner this code is compared to the previous version. Now we have small focused functions that do exactly what the function name implies.

This is a good point to stop refactoring as it's now in good shape.

Note

We did apply the strategy pattern here to the discount rates. This is a design pattern that allows you to switch between different algorithms at runtime. This is going to allow us to easily add new discount types in the future - or swap in different discount logic for each user type.

But more on the strategy pattern in another article.

Quick recap: key principles

Here's a quick summary of the clean code principles we covered:

PrincipleWhy it MattersKey Action
Remove Comment NoisePrevents outdated info and forces clearer code.Write self-explanatory code; comment the why, not the what.
Meaningful Names & TypesImproves readability and self-documentation.Use descriptive names, reveal intent, follow conventions.
Eliminate Magic NumbersReduces confusion and makes updates easier.Replace hardcoded numbers with named constants (as const).
Single Responsibility (SRP)Makes code modular, testable, and maintainable.Break down functions/components into single-purpose units.

Final thoughts

Writing clean code isn't some mystical art reserved for coding wizards. It boils down to sticking to simple principles that make your code understandable and maintainable. Honestly, the extra effort upfront is tiny compared to the massive payoff.

Think long-term: clean code is your best defense against future headaches. As the graph clearly shows, there's a direct link between code quality and how efficiently you can actually get stuff done.

Technical Debt vs. Development Speed
Illustrating the impact of increasing technical debt on development efficiency.
Higher technical debt leads to slower development

This means you'll not only ship features faster, like a true 10x engineer, but also find maintaining and extending your codebase way less painful down the road.

By writing code in this manner, your future self and your colleagues will thank you. This is the type of giga chad code that you can be proud of and that will impress colleagues and future employers.