Good coding practices for well-structured code
07 November 2026 · 4 min read

Good code is code the next person can change safely, and that next person is often you in six months. These habits apply whether you write a frontend, a backend, a script or a pipeline. None of them depend on a language or framework.
Give every piece one job
A function, class or module should do one thing and be easy to name. If you need "and" to describe it, split it.
# Hard to test and reuse: fetching, validating and saving are mixed together
def process_order(order_id):
order = db.get(order_id)
if not order["items"]:
raise ValueError("empty order")
total = sum(i["price"] * i["qty"] for i in order["items"])
db.save(order_id, total)
# One job each
def validate(order):
if not order["items"]:
raise ValueError("empty order")
def calculate_total(items):
return sum(i["price"] * i["qty"] for i in items)
def process_order(order_id):
order = db.get(order_id)
validate(order)
db.save(order_id, calculate_total(order["items"]))The small functions can be tested without a database, and each can change without breaking the others.
Don't repeat yourself, but don't over-share
When the same logic appears in two places, a fix in one will be forgotten in the other. Move it to one shared place: a function, a utility module, a shared component or a common library.
A useful rule: the first time, just write it; the second time, notice it; the third time, extract it. Extracting too early creates a shared abstraction that fits neither caller. Duplication is cheaper than the wrong abstraction.
This applies beyond code. Keep configuration, constants, error messages, database queries and pipeline steps in one place too. In a CI/CD setup, that means reusable pipeline templates instead of copy-pasted YAML.
Name things clearly
A good name removes the need for a comment.
dbecomesdays_until_release.data2becomesvalidated_orders.handle()becomessend_release_notification().
Name booleans as questions (is_ready, has_access) and functions as actions (calculate_total, send_email).
Keep functions short and flat
Deep nesting is hard to read. Return early instead of nesting.
# Nested
def can_deploy(build):
if build.passed:
if build.approved:
if not build.is_frozen:
return True
return False
# Flat
def can_deploy(build):
if not build.passed:
return False
if not build.approved:
return False
return not build.is_frozenSeparate layers
Keep three concerns apart: how data is stored, the rules of your business, and how things are displayed or exposed.
Frontend: components for display, hooks or services for data, utilities for logic.
Backend: routes or controllers stay thin; services hold the rules; repositories talk to the database.
Scripts: separate the "what to do" from the "how to talk to the outside world".
When layers are separate, you can swap a database, change a UI or test the rules alone.
Write unit tests that earn their keep
Tests are what let you change code without fear. A good unit test is small, fast and independent.
def test_calculate_total_adds_price_times_quantity():
items = [{"price": 10, "qty": 2}, {"price": 5, "qty": 1}]
assert calculate_total(items) == 25
def test_validate_rejects_empty_order():
import pytest
with pytest.raises(ValueError):
validate({"items": []})Some habits that help:
Test behavior, not implementation. A refactor should not break tests that still describe correct behaviour.
One reason to fail. Each test checks one thing, and its name says what.
Cover the edges. Empty input, zero, very large values, and the error paths.
Keep tests independent. No test should rely on another running first.
Run them automatically. Tests only help if the pipeline runs them on every change.
Don't chase 100% coverage. Cover the logic that would hurt if it broke.
Handle errors on purpose
Don't swallow errors silently. Fail early with a clear message, catch only what you can handle, and log enough context to find the cause later.
Keep configuration out of code
Secrets, URLs and environment settings belong in environment variables or a config store, never in source control. This is also what makes the same build deployable to every environment.
Make changes small and reviewable
Small pull requests get better reviews and cause fewer surprises. A review should be able to answer one question: does this change do what it says?
Automate the checks
Let tools enforce the boring rules so reviewers can focus on design.
A formatter (Prettier, Black) ends style debates.
A linter (ESLint, Ruff) catches common mistakes.
Type checking (TypeScript, mypy) catches a whole class of bugs before runtime.
A CI pipeline runs all of the above plus your tests on every pull request.
A quick checklist before you open a pull request
Does each function or module do one thing?
Is any logic duplicated that should be shared?
Are the names clear without comments?
Are there tests for the new logic, including edge cases?
Are secrets and settings kept out of the code?
Do the formatter, linter and tests pass?
Closing thought
Good structure is not about being clever. It is about making the next change easy and safe.