Skip to content

Robust-code checklist

This checklist covers the failure modes that pass unit tests and only bite in production: concurrency, injected failure, more than one process, and more than the happy path. CampusCore-specific rules live in coding-principles.md.

When to run it

Run it before you finish, review or refactor a change that does any of these:

  • runs on the request path (middleware, per-request hooks, anything on every request),
  • runs in a background job (worker, scheduled or EventBridge task, management command),
  • touches an external or cloud resource (S3, SQS, ACM/ELB, a connector API, an outbound fetch),
  • or changes infrastructure.

Each item's trigger is in brackets, and only the matching items apply.

Failure handling

  • Request-path code fails open. [middleware or per-request hooks touching the DB, cache or an external service] Catch the failure and fall back to a safe default, so a dependency blip never becomes a site-wide 500. A health probe short-circuited upstream can stay green while everything behind it fails, so a passing probe never proves the app works.
  • Every outbound call has a timeout and bounded retries. [any network call] Decide the failure behavior explicitly, and never write except: pass around primary behavior.
  • Background jobs fail loud. [workers, scheduled tasks] Surface a failure through the retry budget, an alert or a failed mark, because a job that swallows errors looks healthy while doing nothing. Handle each row independently so one failure never aborts the batch.
  • A large queryset is iterated through closing_iterator, never a bare .iterator(). [a loop over more rows than fit in memory: management commands, worker passes, streaming CSV exports] A bare QuerySet.iterator() leaves its server-side cursor for the garbage collector to CLOSE after an exception, which can abort a later transaction on the same connection. campus_core.db.closing_iterator closes the cursor with the block that opened it, and campus_core/tests/test_iterator_sweep.py fails on any bare site.

Concurrency and idempotency

  • Read-modify-write across a slow gap uses a conditional update. [a worker reads state, does slow work and writes back, or two writers share one row] Transition with a guarded UPDATE … WHERE <precondition-still-holds> that no-ops if the state moved, and never save a whole stale object over a concurrent write.
  • Retryable operations are idempotent. [queue- or schedule-triggered writes] A re-run must not duplicate or corrupt, and a dedup key must be collision-free: hash it, never strip-and-truncate a string, because distinct inputs collapsing to one key return the wrong resource.

Consistency across boundaries

  • Order operations so a partial failure cannot orphan state. [a local record mirrors an external resource: an S3 object, a cert, an external-API entity] Change or delete the external side first, confirm it, then change the local record, and on failure keep the record and retry. Treat an idempotent "already gone" as success.

Untrusted input and trust

  • Validate untrusted input at the boundary, and guard the whole operation. [a server-side fetch of a user- or admin-supplied URL, parsing external data] For an outbound fetch, disable redirect-following and pin the resolved IP, because redirects and DNS rebinding reach internal targets such as cloud metadata and VPC hosts. Normalize external data once at ingest.
  • Verify identity, not reachability or shape. [a trust or routing decision made from an external response] Check an identifier, signature or ownership proof, because a well-formed response is not proof it is the right, authorized thing.

Invariants and observability

  • Enforce an invariant at the strongest layer that can guarantee it. [any rule the code depends on] A DB constraint beats app-side validation, and a platform policy such as an IAM Condition beats app-code discipline. A comment claiming a guarantee the layer does not enforce is worse than none.
  • Make security- and state-relevant changes observable. [changes to access, identity, trust, billing] Emit an audit signal, not a debug log, and never ship a dead safeguard, such as a cache invalidation that no-ops across processes.

Change discipline

  • A fix or refactor gets the same scrutiny as new code. Re-derive the blast radius of every change, cosmetic ones included, because a rename can collide with a resource name or break a public contract.