5 Clean Code Practices You Must Know Before You Start Coding
Practical lessons from years of writing and maintaining code

Clean Code Practices — Generated by AI
5 Clean Code Practices You Must Know Before You Start Coding
Practical lessons from years of writing and maintaining code
In this article, we will walk through five clean code practices that will save you countless hours of debugging and refactoring if you’d known them from day one. These aren’t theoretical concepts — they’re battle-tested habits that make your code readable, maintainable, and less painful to revisit six months later.
Table of Contents
- Naming things properly (it’s harder than it sounds)
- Functions should do one thing
- Avoid deep nesting with early returns
- Comments are a code smell
- Keep your classes small and focused
You can read this article for free by clicking here.
1. Naming Things Properly (It’s Harder Than It Sounds)
Let’s be honest — we’ve all written code like this:
int d; // elapsed time in days
What does d mean? Elapsed time? Days? Distance? Sure, the comment explains it, but why force someone to read a comment when the variable name can tell the story?
int elapsedTimeInDays;
That’s better. But we can go further. Instead of a generic integer, consider using a type that conveys meaning:
Duration elapsedTime;
The rule is simple: a name should answer three questions — why it exists, what it does, and how it’s used. If you need a comment to explain a variable or method name, the name is wrong.
Here’s a bad example:
List<Integer> getList() {
// returns user IDs
}
And the fix:
List<Integer> getUserIds() {
}
Now the method name tells us exactly what we’re getting. No comment needed.
2. Functions Should Do One Thing
I used to write functions that did everything — fetch data, validate it, transform it, save it, and send an email. Then I’d name it something like processOrder. That function was a nightmare to test and debug.
Here’s the principle: a function should do one thing, and do it well. If you can extract another function from it with a meaningful name, you probably should.
Consider this:
public void saveUser(User user) {
validateUser(user);
encryptPassword(user);
persistUser(user);
sendWelcomeEmail(user);
}
Each of these calls is a separate responsibility. If sendWelcomeEmail fails, does that mean the user isn’t saved? Probably not. By separating concerns, we can handle failures independently.
Let’s see a concrete example. Instead of:
public void processOrder(Order order) {
if (order.isValid()) {
double total = 0;
for (Item item : order.getItems()) {
total += item.getPrice() * item.getQuantity();
}
order.setTotal(total);
database.save(order);
emailService.sendConfirmation(order);
}
}
We can write:
public void processOrder(Order order) {
if (!order.isValid()) {
throw new InvalidOrderException(order);
}
calculateTotal(order);
saveOrder(order);
sendConfirmation(order);
}
private void calculateTotal(Order order) {
double total = order.getItems().stream()
.mapToDouble(item -> item.getPrice() * item.getQuantity())
.sum();
order.setTotal(total);
}
Now each function has a single responsibility. Testing becomes trivial — you can unit test calculateTotal without worrying about database or email.
3. Avoid Deep Nesting with Early Returns
Deeply nested code is hard to read, hard to test, and hard to maintain. The solution is simple: return early.
Look at this mess:
public String getDiscount(User user) {
if (user != null) {
if (user.isPremium()) {
if (user.getAge() > 60) {
return "20%";
} else {
return "10%";
}
} else {
return "0%";
}
} else {
throw new IllegalArgumentException("User cannot be null");
}
}
Three levels of nesting. Every if adds cognitive load. Now let’s flatten it:
public String getDiscount(User user) {
if (user == null) {
throw new IllegalArgumentException("User cannot be null");
}
if (!user.isPremium()) {
return "0%";
}
return user.getAge() > 60 ? "20%" : "10%";
}
The logic is identical, but the code is linear. Each guard clause handles an edge case and exits early. The main path is clear and easy to follow.
Pro tip: Use guard clauses for validation, null checks, and error conditions. Keep the happy path unindented.
4. Comments Are a Code Smell
I know this sounds controversial, but hear me out. Comments often exist to compensate for bad code. Instead of writing a comment explaining what a confusing block does, refactor the code to be self-explanatory.
Bad:
// Check if user is eligible for premium discount
if (user.getPoints() > 1000 && user.getAge() > 18 && !user.isBlocked()) {
// apply 15% discount
price = price * 0.85;
}
Better:
if (user.isEligibleForPremiumDiscount()) {
price = price.applyDiscount(0.85);
}
The method name isEligibleForPremiumDiscount tells us exactly what the condition means. No comment needed.
There are valid uses for comments — explaining why a decision was made (business rules, workarounds for third-party bugs) or documenting public APIs. But if you’re commenting what the code does, you’re doing it wrong.
Let’s see another example:
// iterate over all items
for (int i = 0; i < items.size(); i++) {
// add item to cart
cart.add(items.get(i));
}
This comment is noise. Every developer knows what a for loop does. Instead, write:
items.forEach(cart::add);
Shorter, clearer, and no comments needed.
5. Keep Your Classes Small and Focused
The Single Responsibility Principle isn’t just about functions — it applies to classes too. A class should have one reason to change.
I once worked on a UserService class that was 2000 lines long. It handled authentication, profile updates, password resets, email notifications, and billing. Every change risked breaking something unrelated.
Here’s the pattern I use now:
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final EmailService emailService;
public User registerUser(RegistrationRequest request) {
// registration logic
}
public User updateProfile(Long userId, ProfileUpdateRequest request) {
// profile update logic
}
}
If I need to add billing logic, I don’t add it here. I create a BillingService:
public class BillingService {
private final PaymentGateway paymentGateway;
private final InvoiceRepository invoiceRepository;
public Invoice createInvoice(User user) {
// billing logic
}
}
Each class is focused. Testing is easier. Changes are isolated. If billing logic breaks, I know exactly where to look.
A good heuristic: if you can’t describe what a class does in 25 words without using “and” or “or”, it’s too big.
That’s the gist of it. These five practices — good naming, single-responsibility functions, early returns, minimal comments, and small classes — will make your code easier to read, test, and maintain. Start applying them today, and your future self (and your colleagues) will thank you.
Tags: java clean-code software-engineering best-practices refactoring software-development
References:
- Robert C. Martin, Clean Code: A Handbook of Agile Software Craftsmanship
- Martin Fowler, Refactoring: Improving the Design of Existing Code
- Clean Code concepts on Refactoring Guru
To support my work, please follow and clap.
메타데이터
- post_id
- 0c4804ca8dcf
- slug
- 5-clean-code-practices-i-wish-i-knew-when-i-started-0c4804ca8dcf
- url
- https://medium.com/but-it-works-on-my-machine/5-clean-code-practices-i-wish-i-knew-when-i-started-0c4804ca8dcf
- canonical_url
- https://medium.com/but-it-works-on-my-machine/5-clean-code-practices-i-wish-i-knew-when-i-started-0c4804ca8dcf
- author_url
- https://medium.com/@aedemirsen
- status
- ok
- fetched_at
- 2026-06-15 20:49:13