Skip to content

Code Readability & Maintainability

Status: 🟢 Active  |  Owner: Engineering Enablement  |  Last Reviewed: 2025-Q4


Overview

Readable code is the single most important property of a maintainable codebase. Code is read far more often than it is written — it is read during code review, during debugging, during on-call incidents, and by engineers who join the team long after the original author has moved on. Writing for the reader is a professional obligation, not an optional courtesy.

These principles apply universally across all languages and stacks.


Functions and Methods

Single Responsibility

Each function does one thing. If you cannot name a function without using "and" or "or", it does too much. Functions that validate and transform and persist violate this principle.

Size Limits

  • Functions should generally be ≤ 30 lines. This is a guideline, not a hard rule — a 35-line function that is perfectly clear is better than splitting it into two 15-line functions with an unclear interface.
  • Functions exceeding 50 lines require a justifying comment explaining why they cannot be decomposed.
  • Functions exceeding 80 lines must be refactored before the PR can be merged.

Parameter Count

Maximum 4 parameters. When a function genuinely needs more, introduce a parameter object or configuration struct. Avoid Boolean trap parameters (doProcess(order, true, false)) — create named methods instead.

// ❌ Boolean trap — what does true, false mean?
orderService.process(order, true, false, null);

// ✅ Named configuration makes intent clear
OrderProcessingOptions options = OrderProcessingOptions.builder()
    .sendConfirmationEmail(true)
    .validateInventory(true)
    .build();
orderService.process(order, options);

Guard Clauses over Nested Conditionals

Reduce nesting with early returns. Maximum 3 levels of indentation in a function body. If you find yourself at 4+ levels, extract a method.

# ❌ Arrow-shaped code — deeply nested
def process_order(order):
    if order is not None:
        if order.is_valid():
            if order.customer is not None:
                if order.customer.has_payment_method():
                    return complete_order(order)

# ✅ Guard clauses — fail fast, flatten structure
def process_order(order: Order) -> OrderResult:
    if order is None:
        raise ValueError("Order cannot be None")
    if not order.is_valid():
        raise InvalidOrderError(f"Order {order.id} failed validation")
    if order.customer is None:
        raise InvalidOrderError("Order has no associated customer")
    if not order.customer.has_payment_method():
        raise PaymentError(f"Customer {order.customer.id} has no payment method")

    return complete_order(order)

Naming

Names are the primary documentation mechanism. A well-named variable, function, or class explains itself without a comment.

Variable Naming

  • Names should be descriptive at their scope. Short names are fine in small scopes; longer names are required in larger scopes.
  • Avoid abbreviations unless universally understood (url, id, http, dto).
  • Avoid noise words: orderData, orderObject, orderInfo — just order.
  • Booleans read as a question: isActive, hasPermission, canRetry, shouldSendEmail.
// ❌ Poor naming
const d = new Date();
const lst = getOrders();
const flg = checkStatus();

// ✅ Clear naming
const createdAt = new Date();
const pendingOrders = getPendingOrders();
const isPaymentConfirmed = checkPaymentStatus();

Function Naming

  • Functions do things — use verb-noun pairs: calculateTotal(), findOrderById(), sendConfirmationEmail().
  • Boolean-returning functions use is, has, can, should: isEligibleForDiscount().
  • Avoid vague names: process(), handle(), manage(), doStuff().

Constants and Magic Values

Never use unexplained numeric or string literals in logic. Name every constant.

# ❌ Magic numbers
if retry_count > 3:
    raise MaxRetriesExceeded()
time.sleep(0.5)

# ✅ Named constants
MAX_RETRY_ATTEMPTS = 3
RETRY_BACKOFF_SECONDS = 0.5

if retry_count > MAX_RETRY_ATTEMPTS:
    raise MaxRetriesExceeded(f"Exceeded {MAX_RETRY_ATTEMPTS} retry attempts")
time.sleep(RETRY_BACKOFF_SECONDS)

Code Organisation

Stepdown Rule

Organise code top-to-bottom so that the reader can understand it from the most abstract to the most concrete, without needing to jump around. Public methods first, private helpers below.

Horizontal Alignment

Do not align variable assignments vertically — it creates maintenance burden and produces noisy diffs.

// ❌ Vertical alignment — noisy diffs when names change
String  firstName  = "Alice";
String  lastName   = "Smith";
Integer orderCount = 5;

// ✅ Standard spacing
String firstName = "Alice";
String lastName = "Smith";
int orderCount = 5;

Logical Grouping

Group related lines of code together. Separate logically distinct operations with a blank line. This makes the structure of a function readable as paragraphs.


Complexity Management

Cyclomatic Complexity

  • Maximum cyclomatic complexity per function: 10 (enforced by SonarQube).
  • Target: ≤ 5 for most functions.
  • Functions with complexity > 10 must be refactored before merge.

Cognitive Complexity

SonarQube also measures cognitive complexity (how hard code is to understand). Maximum: 15 per function.

Avoid Clever Code

Prefer clarity over cleverness. The next engineer maintaining your code may not share your context. Idiomatic language features are encouraged; obscure one-liners are not.

# ❌ Clever but opaque
result = next((x for x in items if x.id == target_id), None) or raise_not_found()

# ✅ Clear intent
def find_item_by_id(items: list[Item], target_id: str) -> Item:
    matching = [item for item in items if item.id == target_id]
    if not matching:
        raise ItemNotFoundError(f"No item found with id: {target_id}")
    return matching[0]

Error Handling

  • Never silently swallow exceptions. An empty catch block is almost always a bug.
  • Log exceptions with context — include the operation being attempted and relevant identifiers.
  • Fail fast — validate inputs at the boundary of a function before processing.
  • Use domain exceptions — prefer specific exception types over generic ones (OrderNotFoundException over RuntimeException).
  • Let exceptions propagate when you cannot meaningfully recover — do not catch just to re-throw a generic exception that loses the stack trace.
// ❌ Swallowed exception
try {
    processOrder(order);
} catch (Exception e) {
    // do nothing
}

// ❌ Lossy exception wrapping
try {
    processOrder(order);
} catch (OrderException e) {
    throw new RuntimeException("Failed"); // lost original cause
}

// ✅ Contextual exception handling
try {
    processOrder(order);
} catch (OrderNotFoundException e) {
    log.warn("Order {} not found during processing", order.getId(), e);
    throw e; // let caller decide
} catch (PaymentException e) {
    log.error("Payment failed for order {}: {}", order.getId(), e.getMessage(), e);
    throw new OrderProcessingException("Payment failure for order " + order.getId(), e);
}

Code Duplication

  • Rule of Three: The first time you write a piece of logic, write it inline. The second time, note the duplication. The third time, extract an abstraction.
  • Do not introduce premature abstractions — a small amount of duplication is often better than the wrong abstraction.
  • SonarQube flags code blocks with > 10 duplicated lines. Flagged duplications require resolution before merge.

References


Last reviewed: 2025-Q4  |  Owner: Engineering Enablement