Who this is for
Backend engineers who want their code to be easy to read, change, review, and operate in production. Useful for solo developers, small teams, and large orgs.
Prerequisites
- Basic proficiency in at least one backend language.
- Familiarity with Git (branching, commits, pull requests).
- Ability to run tests locally.
Why this matters
Maintainable code standards reduce bugs, speed up reviews, and make on-call life calmer. Real tasks where this matters:
- Reviewing pull requests and understanding changes quickly.
- Debugging incidents at 2 a.m. with helpful logs and clear structure.
- Safely evolving APIs and database schemas without breaking clients.
- Onboarding new teammates without endless walkthroughs.
Concept explained simply
Maintainable code is code that another engineer can safely modify after reading it once. Standards are the shared rules that make this possible—naming conventions, structure, error handling, tests, docs, and automation.
Mental model: the 2 a.m. teammate
Imagine a teammate fixing a production issue at 2 a.m. Ask: Can they find the entry point? Understand intent from names? Trust tests? Follow logs to the root cause? Roll back safely?
The "7S" baseline (quick checklist)
- Small functions and files.
- Single responsibility per module.
- Self-documenting names + minimal, precise comments.
- Standard project structure.
- Safe changes via tests and migration/rollback plans.
- Sound error handling and structured logging.
- Style automation (formatter + linter) in CI.
Worked examples
1) Refactor for clarity and safety
Before
def p(u):
r = db.get(u)
if r != None:
if r['st'] == 'a':
r['pts'] += 1
db.save(u, r)
return r
else:
return None
else:
return None
After (maintainable)
from typing import Optional, Dict
class NotActiveError(Exception):
pass
def increment_points(user_id: str) -> Dict:
"""Increment points for an active user.
Raises:
KeyError: when user not found
NotActiveError: when user exists but is not active
"""
user = db.get(user_id)
if user is None:
raise KeyError(f"user not found: {user_id}")
if user["st"] != "a":
raise NotActiveError(f"user not active: {user_id}")
user["pts"] += 1
db.save(user_id, user)
logger.info("points_incremented", extra={"user_id": user_id, "points": user["pts"]})
return user
- Clear names and types.
- Early returns/exceptions, explicit errors.
- Structured log with context.
- Docstring sets expectations.
2) Consistent API errors and logs
Pattern
# Response shape
{
"error": {
"code": "NOT_ACTIVE",
"message": "User is not active"
}
}
# Logging shape
logger.warning("api_error", extra={
"route": "/users/:id/points",
"code": "NOT_ACTIVE",
"user_id": user_id
})
Keep error codes stable for clients, and ensure logs include route, code, and key IDs.
3) Commit + PR standards
Example
feat(points): increment only for active users
- Add NotActiveError and explicit error handling
- Return stable API error code NOT_ACTIVE
- Log with structured context (route, code, user_id)
- Tests: happy path, not found, not active
PR description includes what changed, why, and how you tested it.
4) Safe database migrations
Example
# 2026-01-20_add_points_default.sql
ALTER TABLE users ADD COLUMN points INT NOT NULL DEFAULT 0;
-- Rollback plan
-- ALTER TABLE users DROP COLUMN points;
- Name includes date and purpose.
- Default value avoids NULL surprises.
- Rollback plan documented.
Set up your team standards (step-by-step)
Step 1 — Decide on structure
Choose a project layout (src/, tests/, migrations/, docs/). Add a short README describing the layout.
Step 2 — Automate style
Pick a formatter and linter. Add them to CI so pull requests must pass.
Step 3 — Define API error conventions
Pick stable error codes and response shape. Document in docs/errors.md.
Step 4 — PR and commit templates
Create a PR template and a commit message guideline. Include test plan and risk notes.
Step 5 — Testing and migrations
Require tests for new logic. For schema changes, include forward + rollback scripts.
Quick checklists
Before you push
- [ ] Code formatted; linter passes.
- [ ] Function and variable names reflect intent.
- [ ] Logs include IDs and context, no secrets.
- [ ] Errors return stable codes/messages.
- [ ] Tests cover happy path + main edge cases.
- [ ] README or docs updated when behavior changes.
Pull request
- [ ] Title states what changed.
- [ ] Description explains why and how you tested.
- [ ] Risk/rollback plan noted (especially for migrations).
- [ ] Small, focused diff (or clearly scoped if large).
Exercises you'll do
- Exercise 1: Refactor a messy function to meet standards (naming, errors, logging, docstring, types). Add a small test outline.
- Exercise 2: Write a standards-compliant commit message and PR description for an API change with tests and risk notes.
See the Exercises section below for full instructions and solutions.
Common mistakes and self-check
- Monster functions: If a function is hard to name, it probably does too much. Split it.
- Inconsistent names: Use the same terms throughout (e.g., user_id vs uid). Search your codebase to confirm consistency.
- Comments instead of code: Prefer clear names and small functions. Keep comments for why, not what.
- Unstructured logs: Free-text logs are hard to query. Add key-value context (IDs, codes).
- Hidden side effects: Make state changes explicit and documented. Avoid surprising global mutations.
- No rollback plan: For migrations and risky changes, always include how to revert.
Self-check mini audit (5 minutes)
- Pick one file. Can you summarize each function in one sentence?
- Find one log line. Does it include enough context to trace a request?
- Open a recent PR. Does the description explain risk and testing?
Practical projects
- Create a short code standards doc for your repo (one page). Include naming, logging, errors, tests, and PR template.
- Refactor one business-critical function using the 7S baseline. Add tests and logs.
- Set up formatter + linter + tests in CI. Make the pipeline fail on style or test issues.
- Write an Architecture Decision Record (ADR) for error code conventions.
Learning path
- Adopt a formatter and linter; fix a few files.
- Define error and logging conventions; update one endpoint.
- Add tests for critical paths; measure confidence by removing a bug.
- Create PR and commit templates; review two PRs using them.
- Document standards in the repo; keep it short and enforced.
Next steps
- Apply these standards to a new feature end-to-end.
- Host a short team review to agree on any tweaks.
- Automate enforcement in CI for consistency.
Mini challenge
In 20 minutes, take a small module and:
- Rename variables and functions for clarity.
- Add one structured log and one explicit error.
- Write a two-sentence README note about how to modify it safely.
Quick test
Take the short test to check your understanding. Available to everyone; only logged-in users get saved progress.