Clean Code Principles: Writing Maintainable Software
A practical guide to clean code: meaningful names, short functions, DRY, SOLID foundations, and habits that make codebases easier to read and maintain.
Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.
Clean Code Principles
Introduction
Clean code is code that is easy to understand, easy to change, and easy to test. It is not about being clever — it is about being clear. This guide walks through the foundational habits that make a codebase sustainable.
Meaningful Names
Names are the most important form of documentation in code.
Use Intention-Revealing Names
# Bad
x = 10 # what is x?
# Good
days_until_expiration = 10
# Bad
def calc(a, b):
return a * b
# Good
def calculate_total_price(quantity, unit_price):
return quantity * unit_price
Avoid Disinformation
# Bad
account_list = {} # it's a dict, not a list
# Good
accounts_by_id = {}
Use Pronounceable Names
# Bad
gen_ymdhms = datetime.now()
# Good
generation_timestamp = datetime.now()
Pick One Word Per Concept
| Concept | Pick One | Avoid Mixing |
|---|---|---|
| Fetch data | get, fetch | Don’t use both |
| Create object | create, make, build | Pick one |
| Insert data | insert, add, append | Pick one |
Short Functions
Functions should do one thing, do it well, and do it only.
The Single Responsibility Rule
# Bad: one function does validation, calculation, and persistence
def process_order(order):
if not order.items:
raise ValueError("Empty order")
total = sum(item.price * item.qty for item in order.items)
if order.customer.is_vip:
total *= 0.9
db.execute("INSERT INTO orders ...", total)
send_email(order.customer.email, f"Order {total} confirmed")
# Good: compose small functions
def validate_order(order):
if not order.items:
raise ValueError("Empty order")
def calculate_total(order):
total = sum(item.price * item.qty for item in order.items)
return apply_vip_discount(total, order.customer)
def apply_vip_discount(total, customer):
return total * 0.9 if customer.is_vip else total
def save_order(order, total):
db.execute("INSERT INTO orders ...", total)
def confirm_order(order, total):
validate_order(order)
total = calculate_total(order)
save_order(order, total)
send_email(order.customer.email, f"Order {total} confirmed")
Keep Functions Short
Aim for 20 lines or fewer. If a function exceeds this, it is likely doing more than one thing.
Minimize Parameters
| Number of Args | Readability |
|---|---|
| 0-1 | Ideal |
| 2 | Reasonable |
| 3 | Suspicious |
| >3 | Requires justification (use a struct/object) |
DRY — Don’t Repeat Yourself
Duplication is the root of maintenance pain. When logic is repeated, a bug fix in one place often misses the others.
# Bad: repeated validation logic
def create_user(email, password):
if "@" not in email:
raise ValueError("Invalid email")
...
def update_user_email(user_id, email):
if "@" not in email:
raise ValueError("Invalid email")
...
# Good: extract shared logic
validate_email(email):
if "@" not in email:
raise ValueError("Invalid email")
def create_user(email, password):
validate_email(email)
...
def update_user_email(user_id, email):
validate_email(email)
...
Comments
Comments should explain why, not what. The code itself should explain the what.
# Bad: comment restates the obvious
count = count + 1 # increment count
# Bad: comment explains what the code does
# Check if user is active and has permission
if user.is_active and user.has_permission("read"):
...
# Good: comment explains why
# Skip inactive users because they may have stale permissions
# after an offboarding delay (see policy HR-2024-03)
if user.is_active and user.has_permission("read"):
...
Prefer Self-Documenting Code
# Bad
# returns 1 if the user can access the resource
if check(u, r) == 1:
...
# Good
if user.can_access(resource):
...
Error Handling
Errors are part of the domain, not an afterthought.
Use Exceptions, Not Return Codes
# Bad
def read_file(path):
if not os.path.exists(path):
return None # caller must check for None
return open(path).read()
result = read_file("config.txt")
if result is None:
... # error handling scattered
# Good
def read_file(path):
if not os.path.exists(path):
raise FileNotFoundError(f"{path} not found")
return open(path).read()
try:
content = read_file("config.txt")
except FileNotFoundError as e:
logger.error(e)
...
Don’t Swallow Exceptions
# Bad
try:
risky_operation()
except Exception:
pass # silent failure
# Good
try:
risky_operation()
except NetworkError as e:
logger.warning("Network issue, will retry", exc_info=e)
retry()
Formatting
Consistency matters more than the specific style. Pick a standard, automate it, and move on.
- Use a linter/formatter (Prettier, Black, gofmt)
- Keep related code vertically close — declaration and usage should be near each other
- Limit line length — 80-100 characters is a readable range
- Use blank lines to separate logical groups
Objects and Data Structures
Tell, Don’t Ask
# Bad: asking about state, then deciding
if account.status == "overdrawn":
account.lock()
# Good: tell the object what to do
account.check_overdrawn_and_lock()
The Law of Demeter
A method should only call:
- Methods on itself
- Methods on parameters
- Methods on objects it creates
- Methods on direct components (fields)
# Bad: navigating deep into an object graph
customer.orders[-1].items[0].price
# Good: encapsulate the navigation
customer.last_order_first_item_price()
What Works
- Leave the code cleaner than you found it (Boy Scout Rule)
- Delete dead code — commented-out code, unused functions, unreachable branches
- Write tests first — they force you to write testable (hence clean) code. See testing strategies.
- Code is read 10x more than it is written — optimize for the reader
- Pair programming — two eyes catch complexity before it compounds. Complements code reviews.
Common Mistakes
- Optimizing for brevity instead of clarity
- Using abbreviations that only the author understands
- Functions with side effects that surprise the caller
- Magic numbers and strings scattered throughout the code
- Comments that drift out of sync with the code they describe
- Deep nesting (“arrow code”) that obscures the happy path
Frequently Asked Questions
Q: Should I refactor legacy code that isn’t broken? A: Follow the Boy Scout Rule: clean up the parts you touch. Don’t start large rewrites without business justification and test coverage.
Q: How do I convince my team to adopt clean code practices? A: Start with automated formatting (zero debate), then introduce code review checklists. Show concrete examples of bugs caused by unclear code.
Q: Is clean code slower to write? A: Slightly slower to write, considerably faster to read, debug, and change. The investment pays off within the first modification.
How do I get started with this in an existing project?
Start with a small, isolated part of your codebase. Apply the concepts from this guide to one module or service. Measure the impact, then expand to other areas.
What tools do I need?
The tools mentioned throughout this guide are listed in each section. Most are open-source and widely adopted. Check the related resources for setup instructions.
How do I measure success after implementing this?
Define clear metrics before starting: performance benchmarks, error rates, or maintainability indicators. Compare before and after. Iterate based on the data, not on assumptions.
Related Resources
SOLID Principles Explained with Examples
Learn the five SOLID principles with practical code examples: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion.
GuideWhat Works in Code Review — For Authors and Reviewers
A practical guide to useful code reviews: how to write reviewable code, give constructive feedback, and keep reviews fast and focused.
GuideSoftware Testing Strategy Guide
A practical guide to building a layered testing strategy with unit, integration, and end-to-end tests.
DocUser Story and Acceptance Criteria Template
A user story template that connects user needs to implementation with clear acceptance criteria, definition of done, and INVEST principles.
GuideTest-Driven Development (TDD) — A Practical Workflow
Learn TDD step by step: write a failing test, make it pass, refactor. Red-Green-Refactor with real examples in Python, JavaScript, and Java.