Being good at coding and writing good code are two different skills. I learned it the hard way after one of my pull requests at Amazon received 30+ comments.
At first, it felt overwhelming. But looking back, it turned out to be one of the most valuable learning experiences of my career.
It taught me that writing code isn’t just about passing tests, it’s about writing code that other developers can understand, maintain, and build upon.
The good news is that writing clean code is a skill that can be learned by internalizing a few key principles and applying them consistently in your work.
In this article, I will share 10 practical clean code tips that I’ve picked up over years from senior engineers and reading books like “Clean Code“.
📣 CodeRabbit: Free AI Code Reviews in CLI (Sponsored)
CodeRabbit CLI is an AI code review tool that runs directly in your terminal. It provides intelligent code analysis, catches issues early, and integrates seamlessly with AI coding agents like Claude Code, Codex CLI, Cursor CLI, and Gemini to ensure your code is production-ready before it ships.
Enables pre-commit reviews of both staged and unstaged changes, creating a multi-layered review process.
Fits into existing Git workflows. Review uncommitted changes, staged files, specific commits, or entire branches without disrupting your current development process.
Reviews specific files, directories, uncommitted changes, staged changes, or entire commits based on your needs.
Supports programming languages including JavaScript, TypeScript, Python, Java, C#, C++, Ruby, Rust, Go, PHP, and more.
Offers free AI code reviews with rate limits so developers can experience senior-level reviews at no cost.
Flags hallucinations, code smells, security issues, and performance problems.
Supports guidelines for other AI generators, AST Grep rules, and path-based instructions.
1. Avoid Magic Numbers and Strings
Magic numbers (and strings) are hard-coded values that appear directly in your code without explanation. They make the code cryptic, harder to read, and more error-prone.
If another developer (or even future-you) stumbles upon a random 86400
in the code, it’s not immediately clear whether it represents seconds in a day, milliseconds in a minute, or something else entirely.
Bad Example (Using Magic Numbers):
At first glance, nobody knows what 86400 means. It requires prior knowledge or extra digging.
Good Example (Using Named Constants):
Now, the meaning is crystal clear. Even someone new to the code can immediately understand that the session expires if more than a day has passed.
2. Use Meaningful, Descriptive Names
Code is read far more often than it is written. If variables, methods, or classes have vague names, the reader is forced to pause and decipher what’s happening. This slows everyone down and increases the chances of mistakes.
Your names should reveal intent. A reader should understand what a piece of code does without needing extra comments or mental gymnastics.
Bad Example (Vague Names):
Here, the class C
, method m1
, and variables a
, b
, r
mean nothing to the reader. What does m1
do? What are a
and b
supposed to represent?
Good Example (Meaningful Names):
Now it’s immediately clear: we are dealing with a Rectangle
, and the method calculates and prints the area.
Consistency Matters
Consistency in naming is equally important. If you sometimes call a user’s identifier id
, elsewhere userId
, and in another place uid
, you’re inviting confusion.
Bad Example (Inconsistent Naming):
String id = "123";
String userIdentifier = "123";
String uid = "123";
Which one is the real user ID? The inconsistency makes the code harder to follow.
Good Example (Consistent Naming):
String userId = "123";
Use the same term across your codebase. This helps new developers quickly build a mental model of your system.
Caller Perspective
When naming methods, think about how they’ll be used by the caller.
Bad Example:
order.process();
Does process()
mean "validate"? "Charge payment"? "Ship the product"? It’s too vague.
Good Example:
order.chargePayment();
order.shipToCustomer();
Now the intent is obvious from the caller’s perspective.
3. Favor Early Returns Over Deep Nesting
Deeply nested if
conditions or loops make code harder to read and understand. The more levels of indentation you add, the more cognitive load you place on the reader. It becomes easy to miss edge cases or introduce bugs.
Instead, you can flatten your code by returning early when conditions are not met. This improves readability, reduces indentation, and makes your code easier to follow.
Bad Example (Deep Nesting):
This works, but the nested if
blocks make it harder to see the main logic.
Good Example (Early Returns):
Now the method reads like a sequence of clear checks. Once the conditions are satisfied, the main action (shipOrder
) is executed without unnecessary nesting.
4. Avoid Long Parameter Lists
Functions with long parameter lists are hard to read, hard to remember, and error-prone. When you see a method call with 6–7 arguments, it’s not obvious which parameter represents what especially if multiple arguments are of the same type.
Long parameter lists also make functions difficult to extend or refactor. If you need to pass additional information later, the list keeps growing, further complicating the method signature.
Bad Example (Too Many Parameters):
Calling this method becomes messy:
At a glance, it’s not obvious which argument corresponds to what.
Good Example (Group into Object):
Calling the method now looks much cleaner:
Even Better: Use the Builder Pattern
When the object has many optional fields, a builder improves clarity and flexibility.
Now the method call is self-documenting. You can easily see what values are being set without guessing their order.
5. Keep Functions Small and Focused
Large, multi-purpose functions are hard to read, test, and maintain. They often mix unrelated responsibilities, which makes them fragile and changing one part can unintentionally break another.
A good rule of thumb is: One function, one responsibility.
If you can’t summarize what a function does in a single sentence, it’s probably doing too much.
Bad Example (Too Many Responsibilities):
This single method is handling validation, calculation, payment, and shipping. If something breaks, debugging will be messy.
Good Example (Single Responsibility):
Now each function has a clear, focused responsibility:
validateOrder()
→ checks inputcalculateTotal()
→ computes the costchargePayment()
→ handles paymentshipOrder()
→ manages shipping
This not only improves readability but also makes the code easier to test and maintain.
By breaking logic into smaller functions, you can reuse them elsewhere. For example, calculateTotal()
could be reused in an invoice service without duplicating logic.
6. Keep Code DRY
DRY stands for Don’t Repeat Yourself. Duplicate code is dangerous because:
If you find a bug, you have to fix it in every copy.
Changes in requirements often lead to inconsistencies if one copy gets updated but others don’t.
The codebase grows unnecessarily large and harder to maintain.
The goal isn’t to remove all duplication at any cost, but to identify meaningful abstractions and reuse logic where it makes sense.
Bad Example (Duplicate Logic):
Here, both services have the same calculation logic duplicated. If you need to update the way totals are calculated (e.g., apply discounts or taxes), you must change it in multiple places.
Good Example (Extract Common Logic):
Now the logic lives in one place (PriceCalculator
). Any change is made once and applied everywhere.
7. Apply KISS Principle
KISS stands for “Keep It Simple, Stupid.” It reminds us that software should be as simple as possible, and no simpler.
Overengineering—adding unnecessary complexity, abstractions, or features creates code that is:
Harder to read and maintain
More prone to bugs
Slower to adapt when requirements change
Many times, developers try to anticipate future needs by writing overly generic or abstract code. Ironically, this extra complexity often becomes useless because requirements evolve differently than expected.
Bad Example (Unnecessary Complexity):
This design uses strategy pattern for something as trivial as addition and subtraction. Unless you’re genuinely dealing with dozens of complex calculation types, this abstraction is overkill.
Good Example (Keep It Simple):
Much simpler, easier to read, and does the job perfectly without unnecessary layers.
8. Prefer Composition Over Inheritance
Inheritance is one of the pillars of OOP, but when misused, it leads to tight coupling, fragile hierarchies, and code that’s hard to extend or change.
With inheritance, a subclass is tightly bound to the implementation details of its parent.
Changes in the base class can ripple through all subclasses, causing unexpected breakages.
Deep inheritance chains are difficult to understand and maintain.
Composition, on the other hand, favors building classes by combining smaller, reusable components. It leads to flexible and loosely coupled designs.
Bad Example (Overusing Inheritance):
Here, Bicycle
inherits from Vehicle
and gets a drive()
method that may not even make sense. This is a design smell caused by forcing everything into an inheritance tree.
Good Example (Using Composition):
Usage:
Car car = new Car(new Engine());
car.move(); // Driving with engine...
Bicycle bike = new Bicycle(new Pedal());
bike.move(); // Pedaling forward...
Instead of forcing all vehicles into one inheritance tree, we compose behavior using interfaces and delegation. Each class is free to define its own functionality without being constrained by a parent class.
When to Use Inheritance vs Composition
Use inheritance when: there’s a clear “is-a” relationship and the base class is stable (e.g.,
Rectangle extends Shape
).Use composition when: you want to add behaviors flexibly, avoid tight coupling, or support multiple behaviors (e.g., a
Car
has anEngine
).
9. Comment Only When Necessary
Many developers either don’t comment at all or over-comment every line. Both are problematic:
Too few comments make it hard to understand non-obvious decisions.
Too many comments clutter the code and often go out of date, making them misleading.
The truth is: good code should explain itself through meaningful names, small functions, and clear structure. Comments should be reserved for explaining why something is done, not what the code is already showing.
Bad Example (Redundant Comments):
// Create a new user
User user = new User();
// Set the user's name
user.setName("John");
// Save the user to the database
userRepository.save(user);
These comments add no value, they just repeat what the code already says. Worse, if the method names change (setName
→ setFullName
), the comments may become inconsistent.
Good Example (Self-Explanatory Code):
User newUser = new User();
newUser.setFullName("John Doe");
userRepository.save(newUser);
Here, the code is clear enough that comments are unnecessary.
Comment the Why, Not the What
Sometimes, though, there is value in comments when explaining intent or non-obvious decisions.
Example:
// We cache user details to reduce DB load, even if slightly stale data is acceptable.
// Freshness guaranteed by scheduled background refresh every 5 minutes.
UserDetails userDetails = userCache.get(userId);
Here the comment explains why caching is acceptable, something that code alone cannot convey.
10. Write Good Commit Messages
A commit message is more than just a note for yourself, it’s documentation for your whole team (and your future self). Poor commit messages like “fix bug” or “changes” provide no context, making it hard to understand the history of a project or why a change was made.
Clear commit messages:
Help others (and you) understand the why behind code changes.
Make debugging and rollback easier.
Improve collaboration, especially in large teams.
Guidelines for Good Commit Messages
Use the imperative mood
Pretend you’re giving a command: “Add user validation” instead of “Added user validation”.
This matches how Git itself uses messages (e.g., “Merge branch…”).
Keep the subject line concise
Aim for 50 characters or less.
Capitalize the first letter, don’t end with a period.
Provide context in the body (if needed)
The subject is what changed.
The body explains why it changed or how it was solved.
Wrap lines at ~72 characters for readability in CLI tools.
Reference issues or tickets (if applicable)
Helps track changes to features, bugs, or requirements.
Bad Examples
fix bug
changes
stuff
These are vague and unhelpful.
Good Examples
Short Commit (Simple Change):
Add null check in user validation
Commit with Body (Explaining Reason):
Refactor payment service to use strategy pattern
Replaced conditional logic with a Strategy pattern to make it easier
to add new payment providers in the future. This improves extensibility
and reduces duplication across services.
Commit with Issue Reference:
Fix incorrect tax calculation in invoice generator (#123)
The tax rate was being applied twice in certain edge cases. Added
unit tests to cover this scenario.
Code Example
Suppose you refactored this:
// Old Code
double total = price + (price * 0.18); // 18% GST hardcoded
To this:
// New Code
private static final double GST_RATE = 0.18;
double total = price + (price * GST_RATE);
A bad commit would be:
fix tax
A good commit would be:
Refactor invoice calculation to use GST_RATE constant
Replaced hardcoded tax value with a named constant. This makes
the code easier to maintain if tax rates change in the future.
Thank you for reading!
If you found it valuable, hit a like ❤️ and consider subscribing for more such content.
If you have any questions or suggestions, leave a comment.
P.S. If you’re enjoying this newsletter and want to get even more value, consider becoming a paid subscriber.
As a paid subscriber, you'll unlock all premium articles and gain full access to all premium courses on algomaster.io.
There are group discounts, gift options, and referral bonuses available.
Checkout my Youtube channel for more in-depth content.
Follow me on LinkedIn, X and Medium to stay updated.
Checkout my GitHub repositories for free interview preparation resources.
I hope you have a lovely day!
See you soon,
Ashish
Good one . Thanks
Concise explanation to achieve good and maintainable code. Thank you