Cognitive Load — Practice Tasks¶
12 hands-on exercises that each take a real fragment of high-cognitive-load code and reduce it to something a tired reviewer can hold in their head at 5pm on a Friday. Every task gives you a scenario, the offending code (varied across Go / Java / Python), a precise instruction, and a collapsible solution with the reasoning — including before/after complexity numbers where they sharpen the point.
Cognitive load is the working-memory tax a reader pays to understand code. Humans hold roughly 4 (±1) chunks at once. Every live variable, every open if, every implicit side effect is a chunk. The goal of these tasks is never "fewer lines" — it is fewer simultaneous things to track. Sometimes that means more lines.
Table of Contents¶
- Task 1 — Flatten deep nesting with guard clauses (Go)
- Task 2 — Replace a positional param list with an options object (Python)
- Task 3 — Unpack a clever one-liner into named steps (Python)
- Task 4 — Kill the boolean flag parameter (Java)
- Task 5 — Make hidden control flow explicit: no exception-as-flow (Python)
- Task 6 — A getter that lies: remove side effects from reads (Java)
- Task 7 — Chunk a screen-long function into named steps (Go)
- Task 8 — Lift mixed abstraction levels (Python)
- Task 9 — Rename acronym soup (Java)
- Task 10 — Two boolean flags, four behaviors → an enum + dispatch (Go)
- Task 11 — When the clever one-liner should stay (Python)
- Task 12 — Full cognitive-load audit (Java — open-ended)
- Self-Assessment
- Related Topics
How to Use¶
- Read the scenario first, then the code. Cognitive load is contextual — the same code can be fine in a throwaway script and toxic in a hot path that ten engineers touch.
- Cover the solution. Open
<details>only after you have written your own version. The act of re-deriving the fix is where the learning happens. - Count chunks, not lines. Before and after each rewrite, ask: how many things must I hold in my head to be sure this is correct? That number — not the diff size — is the score.
- Run it. Every snippet is intended to compile/run with trivial stubs filled in. Paste it into a scratch file and execute the "before" and "after" to confirm behavior is preserved.
- Order is deliberate. Tasks run easy → hard. The last two ask you to judge (when a one-liner earns its keep) and to audit (find every smell yourself).
The mental model used throughout:
Task 1 — Flatten deep nesting with guard clauses (Go)¶
Difficulty: Easy
Scenario: This validation+save function for a checkout service sits four if levels deep. To read line 9 a reviewer must remember three conditions are simultaneously true. The "happy path" — the thing the function is actually for — is buried at the bottom, indented off the screen.
func PlaceOrder(cart *Cart, user *User) (*Order, error) {
if user != nil {
if user.Verified {
if len(cart.Items) > 0 {
if cart.Total() <= user.CreditLimit {
order := newOrder(cart, user)
if err := save(order); err == nil {
return order, nil
} else {
return nil, err
}
} else {
return nil, errors.New("over credit limit")
}
} else {
return nil, errors.New("empty cart")
}
} else {
return nil, errors.New("user not verified")
}
} else {
return nil, errors.New("no user")
}
}
Instruction: Invert each condition into a guard clause that returns early, so the happy path is un-indented and reads top-to-bottom. Behavior must be identical.
Solution
func PlaceOrder(cart *Cart, user *User) (*Order, error) {
if user == nil {
return nil, errors.New("no user")
}
if !user.Verified {
return nil, errors.New("user not verified")
}
if len(cart.Items) == 0 {
return nil, errors.New("empty cart")
}
if cart.Total() > user.CreditLimit {
return nil, errors.New("over credit limit")
}
order := newOrder(cart, user)
if err := save(order); err != nil {
return nil, err
}
return order, nil
}
Task 2 — Replace a positional param list with an options object (Python)¶
Difficulty: Easy
Scenario: A call site reads connect("db.internal", 5432, 30, 10, True, False, True, 3). No human can verify that argument list is correct without scrolling to the signature and counting on their fingers. Booleans in particular are unreadable positionally.
def connect(
host: str,
port: int,
connect_timeout: int,
pool_size: int,
use_tls: bool,
verify_cert: bool,
keepalive: bool,
max_retries: int,
) -> Connection:
...
# call site:
conn = connect("db.internal", 5432, 30, 10, True, False, True, 3)
Instruction: Introduce a frozen dataclass config object with sensible defaults so only host/port are required and every optional knob is named at the call site.
Solution
from dataclasses import dataclass
@dataclass(frozen=True)
class ConnectionConfig:
host: str
port: int = 5432
connect_timeout: int = 30
pool_size: int = 10
use_tls: bool = True
verify_cert: bool = True
keepalive: bool = True
max_retries: int = 3
def connect(config: ConnectionConfig) -> Connection:
...
# call site — every value is self-documenting, order no longer matters:
conn = connect(ConnectionConfig(
host="db.internal",
use_tls=True,
verify_cert=False,
))
Task 3 — Unpack a clever one-liner into named steps (Python)¶
Difficulty: Easy
Scenario: A teammate is proud of this one-liner that "computes the report." You have stared at it for ninety seconds and still cannot say what it returns when a department has no employees.
result = {d: round(sum(e.salary for e in emps) / len(emps), 2)
for d, emps in groupby(sorted(employees, key=lambda x: x.dept),
key=lambda x: x.dept)
if (emps := list(emps))}
Instruction: Rewrite as a sequence of named steps that a reader can trace one line at a time. Name the intermediate concepts.
Solution
from itertools import groupby
from statistics import mean
def average_salary_by_department(employees) -> dict[str, float]:
by_dept = sorted(employees, key=lambda e: e.dept)
averages: dict[str, float] = {}
for dept, group in groupby(by_dept, key=lambda e: e.dept):
salaries = [e.salary for e in group]
averages[dept] = round(mean(salaries), 2)
return averages
Task 4 — Kill the boolean flag parameter (Java)¶
Difficulty: Medium
Scenario: Code review keeps surfacing calls like mailer.send(message, true). Reviewers cannot tell what true means without opening the method. Worse, the method is an if/else that does two genuinely different jobs glued together by the flag.
public void send(Message msg, boolean isUrgent) {
if (isUrgent) {
validateUrgent(msg);
smsGateway.push(msg.recipient(), msg.body());
pagerDuty.alert(msg);
log.warn("URGENT sent to {}", msg.recipient());
} else {
validateStandard(msg);
emailQueue.enqueue(msg);
log.info("Queued mail to {}", msg.recipient());
}
}
// call sites:
mailer.send(welcome, false);
mailer.send(outage, true);
Instruction: Split the flag into two intention-revealing methods. The shared send should no longer branch on a boolean.
Solution
public void sendUrgent(Message msg) {
validateUrgent(msg);
smsGateway.push(msg.recipient(), msg.body());
pagerDuty.alert(msg);
log.warn("URGENT sent to {}", msg.recipient());
}
public void sendStandard(Message msg) {
validateStandard(msg);
emailQueue.enqueue(msg);
log.info("Queued mail to {}", msg.recipient());
}
// call sites — the verb says what happens:
mailer.sendStandard(welcome);
mailer.sendUrgent(outage);
Task 5 — Make hidden control flow explicit: no exception-as-flow (Python)¶
Difficulty: Medium
Scenario: A profiler flags this lookup as a hot spot. Reading it, you realize control flow leaves the function through an exception on the common path — a cache miss is not exceptional, it is expected. The exception jump is invisible at the call site and makes the function impossible to reason about locally.
def get_user(user_id):
try:
user = cache[user_id]
return user
except KeyError:
try:
user = db.query("SELECT * FROM users WHERE id = ?", user_id)
cache[user_id] = user
return user
except RecordNotFound:
raise UserNotFound(user_id)
except ConnectionError:
return None # silently degrade — caller has no idea
Instruction: Use explicit, value-returning control flow for the expected cases (cache miss, not found). Reserve exceptions for the unexpected case (a real infrastructure failure), and make the degraded path explicit instead of silently returning None.
Solution
def get_user(user_id: str) -> User | None:
cached = cache.get(user_id) # miss is a value, not an exception
if cached is not None:
return cached
user = db.find_user(user_id) # returns None when not found
if user is None:
return None
cache[user_id] = user
return user
def find_user(self, user_id: str) -> User | None:
row = self.query_one("SELECT * FROM users WHERE id = ?", user_id)
return User.from_row(row) if row else None
# A real ConnectionError still propagates — it IS exceptional, and the
# caller (or a circuit breaker) should decide, not this function silently.
Task 6 — A getter that lies: remove side effects from reads (Java)¶
Difficulty: Medium
Scenario: A bug report says the session token "sometimes refreshes for no reason" during read-only reporting. The culprit: a getToken() accessor that mutates state. Readers reasonably assume getX() is free of side effects, so they call it freely in logging, in asserts, in loops — and every call silently does work.
public class Session {
private String token;
private Instant expiresAt;
public String getToken() {
if (Instant.now().isAfter(expiresAt)) {
this.token = authClient.refresh(); // network call inside a getter!
this.expiresAt = Instant.now().plus(Duration.ofHours(1));
auditLog.record("token refreshed");
}
return token;
}
}
Instruction: Make reads pure. Separate the query (give me the current token) from the command (ensure a fresh token). The name of each method must advertise whether it can change state.
Solution
public class Session {
private String token;
private Instant expiresAt;
/** Pure query: never mutates, never does I/O. Safe to call anywhere. */
public String currentToken() {
return token;
}
public boolean isExpired() {
return Instant.now().isAfter(expiresAt);
}
/** Explicit command: may perform a network refresh and mutate state. */
public String ensureFreshToken() {
if (isExpired()) {
this.token = authClient.refresh();
this.expiresAt = Instant.now().plus(Duration.ofHours(1));
auditLog.record("token refreshed");
}
return token;
}
}
// caller is now in control of when the expensive, mutating work happens:
String token = session.ensureFreshToken(); // I accept this may refresh
sendRequest(token);
Task 7 — Chunk a screen-long function into named steps (Go)¶
Difficulty: Medium
Scenario: This request handler is 70 lines tall — it does not fit on one screen, so no reviewer ever sees it whole. It mixes parsing, authorization, business logic, persistence, and response formatting in one undifferentiated wall.
func HandleCreateInvoice(w http.ResponseWriter, r *http.Request) {
// parse
var req CreateInvoiceRequest
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "cannot read body", http.StatusBadRequest)
return
}
if err := json.Unmarshal(body, &req); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}
// authorize
token := r.Header.Get("Authorization")
claims, err := verifyJWT(token)
if err != nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
if !claims.HasRole("billing") {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
// build invoice
inv := Invoice{CustomerID: req.CustomerID, IssuedBy: claims.Subject}
var total int64
for _, line := range req.Lines {
if line.Quantity <= 0 {
http.Error(w, "quantity must be positive", http.StatusBadRequest)
return
}
total += line.UnitPriceCents * int64(line.Quantity)
inv.Lines = append(inv.Lines, line)
}
inv.TotalCents = total
// persist
if err := db.Insert(r.Context(), &inv); err != nil {
http.Error(w, "could not save", http.StatusInternalServerError)
return
}
// respond
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(CreateInvoiceResponse{ID: inv.ID, TotalCents: inv.TotalCents})
}
Instruction: Keep the handler as a short orchestrator that reads as a list of named phases. Extract each comment-block into its own function. Behavior identical.
Solution
func HandleCreateInvoice(w http.ResponseWriter, r *http.Request) {
req, err := parseCreateInvoice(r)
if err != nil {
writeError(w, err)
return
}
claims, err := authorizeBilling(r)
if err != nil {
writeError(w, err)
return
}
inv, err := buildInvoice(req, claims)
if err != nil {
writeError(w, err)
return
}
if err := db.Insert(r.Context(), &inv); err != nil {
writeError(w, apiError{http.StatusInternalServerError, "could not save"})
return
}
writeJSON(w, http.StatusCreated, CreateInvoiceResponse{
ID: inv.ID, TotalCents: inv.TotalCents,
})
}
type apiError struct {
status int
message string
}
func (e apiError) Error() string { return e.message }
func parseCreateInvoice(r *http.Request) (CreateInvoiceRequest, error) {
var req CreateInvoiceRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return req, apiError{http.StatusBadRequest, "invalid json"}
}
return req, nil
}
func authorizeBilling(r *http.Request) (Claims, error) {
claims, err := verifyJWT(r.Header.Get("Authorization"))
if err != nil {
return Claims{}, apiError{http.StatusUnauthorized, "unauthorized"}
}
if !claims.HasRole("billing") {
return Claims{}, apiError{http.StatusForbidden, "forbidden"}
}
return claims, nil
}
func buildInvoice(req CreateInvoiceRequest, claims Claims) (Invoice, error) {
inv := Invoice{CustomerID: req.CustomerID, IssuedBy: claims.Subject}
for _, line := range req.Lines {
if line.Quantity <= 0 {
return Invoice{}, apiError{http.StatusBadRequest, "quantity must be positive"}
}
inv.TotalCents += line.UnitPriceCents * int64(line.Quantity)
inv.Lines = append(inv.Lines, line)
}
return inv, nil
}
func writeError(w http.ResponseWriter, err error) {
if api, ok := err.(apiError); ok {
http.Error(w, api.message, api.status)
return
}
http.Error(w, "internal error", http.StatusInternalServerError)
}
func writeJSON(w http.ResponseWriter, status int, body any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(body)
}
Task 8 — Lift mixed abstraction levels (Python)¶
Difficulty: Hard
Scenario: This function jumps between altitudes mid-sentence: one line is high-level orchestration ("publish the event"), the next is low-level byte twiddling (CRC computation, bit masks). The reader's brain has to keep context-switching between "what is this trying to do" and "how does this bit math work," which is exhausting and error-prone.
def publish_sensor_reading(sensor, reading):
# high level
if not sensor.is_active:
return
# suddenly very low level: pack into a 4-byte frame
raw = (reading.value & 0x0FFF) << 4
raw |= (reading.unit_code & 0x0F)
frame = bytearray(4)
frame[0] = (raw >> 8) & 0xFF
frame[1] = raw & 0xFF
crc = 0xFFFF
for b in frame[:2]:
crc ^= b << 8
for _ in range(8):
crc = (crc << 1) ^ 0x1021 if crc & 0x8000 else crc << 1
crc &= 0xFFFF
frame[2] = (crc >> 8) & 0xFF
frame[3] = crc & 0xFF
# back to high level
topic = f"sensors/{sensor.zone}/{sensor.id}"
broker.publish(topic, bytes(frame))
metrics.increment("readings.published")
Instruction: Make publish_sensor_reading read entirely at one altitude — orchestration only. Push the bit-level frame encoding and CRC down into named helpers (ideally onto the protocol/encoder that owns that knowledge).
Solution
def publish_sensor_reading(sensor, reading):
if not sensor.is_active:
return
frame = encode_frame(reading)
topic = f"sensors/{sensor.zone}/{sensor.id}"
broker.publish(topic, frame)
metrics.increment("readings.published")
# --- low-level protocol details, all at the same (low) altitude ---
def encode_frame(reading) -> bytes:
payload = pack_payload(reading.value, reading.unit_code)
crc = crc16_ccitt(payload)
return bytes([*payload, crc >> 8 & 0xFF, crc & 0xFF])
def pack_payload(value: int, unit_code: int) -> bytes:
raw = (value & 0x0FFF) << 4 | (unit_code & 0x0F)
return bytes([(raw >> 8) & 0xFF, raw & 0xFF])
def crc16_ccitt(data: bytes) -> int:
crc = 0xFFFF
for byte in data:
crc ^= byte << 8
for _ in range(8):
crc = ((crc << 1) ^ 0x1021) if crc & 0x8000 else (crc << 1)
crc &= 0xFFFF
return crc
Task 9 — Rename acronym soup (Java)¶
Difficulty: Hard
Scenario: You inherit this method. Every identifier is an abbreviation, and decoding it requires tribal knowledge nobody wrote down. The author is on parental leave. You need to add a feature here this week.
public BigDecimal calcAdjTxblAmt(Acct a, Txn t, int yr) {
BigDecimal gma = a.getGmaForYr(yr); // ?
BigDecimal exc = t.getExc(); // ?
BigDecimal tdb = gma.subtract(exc); // ?
if (a.getCtgy() == 2 && t.getFlg()) { // magic 2? what flag?
tdb = tdb.multiply(new BigDecimal("0.85"));
}
BigDecimal mtd = tdb.compareTo(a.getStdDed(yr)) > 0
? tdb.subtract(a.getStdDed(yr))
: BigDecimal.ZERO;
return mtd;
}
Instruction: Rename every acronym to a full, domain-meaningful word. Replace magic constants with named constants/enums. Do not change the calculation — only make it legible. State any assumptions you had to make about meaning.
Solution
// Assumptions, stated explicitly because the original hid them:
// gma -> grossMonetaryAmount exc -> exclusion
// tdb -> taxableBase mtd -> taxableAmountAfterDeduction
// ctgy == 2 -> AccountCategory.NONPROFIT
// t.getFlg() -> transaction.isReducedRate()
// 0.85 -> the nonprofit reduced-rate multiplier (15% relief)
private static final BigDecimal NONPROFIT_REDUCED_RATE = new BigDecimal("0.85");
public BigDecimal calculateAdjustedTaxableAmount(Account account, Transaction transaction, int year) {
BigDecimal grossAmount = account.getGrossAmountForYear(year);
BigDecimal exclusion = transaction.getExclusion();
BigDecimal taxableBase = grossAmount.subtract(exclusion);
boolean qualifiesForReducedRate =
account.getCategory() == AccountCategory.NONPROFIT && transaction.isReducedRate();
if (qualifiesForReducedRate) {
taxableBase = taxableBase.multiply(NONPROFIT_REDUCED_RATE);
}
BigDecimal standardDeduction = account.getStandardDeduction(year);
if (taxableBase.compareTo(standardDeduction) <= 0) {
return BigDecimal.ZERO;
}
return taxableBase.subtract(standardDeduction);
}
Task 10 — Two boolean flags, four behaviors → an enum + dispatch (Go)¶
Difficulty: Hard
Scenario: This export function grew a second boolean. The call site export(data, true, false) now encodes one of four modes, and no reader can decode which. Worse, one of the four combinations (compress && !includeHeaders for CSV) is nonsensical but nothing prevents callers from requesting it.
func Export(data []Record, asJSON bool, compress bool) ([]byte, error) {
var out []byte
var err error
if asJSON {
out, err = json.Marshal(data)
} else {
out, err = toCSV(data) // CSV always includes a header row
}
if err != nil {
return nil, err
}
if compress {
out = gzipBytes(out)
}
return out, nil
}
// call site — what does this mean?
blob, _ := Export(records, true, false)
Instruction: Replace the two booleans with a single typed Format enum (the four valid combinations become four named formats), making illegal states unrepresentable and the call site self-describing. Use a dispatch table rather than nested ifs.
Solution
type Format int
const (
FormatJSON Format = iota
FormatJSONGzip
FormatCSV
FormatCSVGzip
)
// encoder describes how to serialize and whether to compress.
type encoder struct {
marshal func([]Record) ([]byte, error)
compress bool
}
var encoders = map[Format]encoder{
FormatJSON: {marshal: json.Marshal, compress: false},
FormatJSONGzip: {marshal: json.Marshal, compress: true},
FormatCSV: {marshal: toCSV, compress: false},
FormatCSVGzip: {marshal: toCSV, compress: true},
}
func Export(data []Record, format Format) ([]byte, error) {
enc, ok := encoders[format]
if !ok {
return nil, fmt.Errorf("unknown export format: %d", format)
}
out, err := enc.marshal(data)
if err != nil {
return nil, err
}
if enc.compress {
out = gzipBytes(out)
}
return out, nil
}
// call site — unambiguous:
blob, _ := Export(records, FormatJSON)
Task 11 — When the clever one-liner should stay (Python)¶
Difficulty: Hard
Scenario: A zealous reviewer wants you to "unpack" every comprehension in the codebase into explicit loops, citing Task 3. But not every dense expression is high cognitive load — some are idiomatic and reading them as a unit is less effort than reading the expanded form. Your job is to judge, not to mechanically expand.
Consider these two candidates flagged in the same review:
# Candidate A
flat = [x for row in matrix for x in row]
# Candidate B
result = next((u for u in users if u.id == target_id and u.active
and u.region in allowed and not u.banned), None)
against the "expanded" alternatives:
# Expanded A
flat = []
for row in matrix:
for x in row:
flat.append(x)
# Expanded B
result = None
for u in users:
if u.id == target_id:
if u.active:
if u.region in allowed:
if not u.banned:
result = u
break
Instruction: Decide for each candidate whether to keep the one-liner or expand it, and justify using the cognitive-load criterion (not personal taste). If you keep a dense form, say what makes it safe; if you expand, say what tipped it over.
Solution
**Candidate A: keep the one-liner.** This is the *canonical* Python idiom for flattening one level of nesting. An experienced reader recognizes the `[x for row in matrix for x in row]` shape as a single chunk — "flatten" — the same way you read the word "flatten" without spelling it. The expanded version is **four lines and two nesting levels** to express one well-known operation; it costs *more* working memory, not less, because the reader has to reconstruct that the loops merely flatten. There are no hidden side effects, no exception flow, no surprising laziness. Density here is communication, not obfuscation. **Candidate B: expand it — but not into the nested-`if` pyramid.** The reviewer is right that B is too much, but the proposed expansion is *worse* (a 4-deep nesting pyramid, exactly the smell from Task 1). The real problem with B is that **four distinct business predicates are crammed into one boolean expression** inside a generator inside a `next(...)` with a default. The fix is to name the predicate, not to explode the control flow: Now the `next(..., None)` idiom — "first match or `None`" — stays as a recognized one-chunk pattern, and the four-part eligibility rule has a *name* that says what it means. A reader scanning the lookup sees `is_eligible`; a reader debugging the rule opens one tiny pure function. Working memory at each site is one chunk. **The judgment criterion.** Keep a dense expression when **the reader can recognize it as a single known idiom faster than they can read the expansion** (Candidate A, and the `next(..., default)` shell of B). Break it up when it **forces the reader to simultaneously hold multiple unrelated facts** (B's four predicates) — but break it up by *naming the concept*, not by adding nesting. The metric is always "chunks the reader must hold," never "characters on the line." Cleverness that compresses a familiar idea is good; cleverness that hides an unfamiliar one is debt.Task 12 — Full cognitive-load audit (Java — open-ended)¶
Difficulty: Hard
Scenario: Below is a realistic method from a payments service. It exhibits most of the anti-patterns in this chapter at once. List every cognitive-load smell you can find, then write a one-line remediation for each. Finally, sketch the refactored top-level method (you may stub the helpers).
public Object proc(Map<String,Object> d, boolean retry, boolean async, boolean dryRun) {
if (d != null) {
if (d.get("amt") != null) {
if ((int)(Integer)d.get("amt") > 0) {
try {
String c = (String)d.get("cur");
int a = (int)(Integer)d.get("amt");
long x = System.currentTimeMillis() & 0xFFFFFFFFL; // "id"
Gateway g = retry ? new RetryGateway(3) : new Gateway();
if (!dryRun) {
if (async) {
executor.submit(() -> g.charge(c, a, x));
return null;
} else {
Receipt r = g.charge(c, a, x);
audit(r); // side effect; also this.lastReceipt = r inside audit
return r;
}
} else {
return new Receipt(x, a, c, "DRY_RUN");
}
} catch (Exception e) {
return null; // swallow everything
}
} else { throw new RuntimeException("bad"); }
} else { throw new RuntimeException("bad"); }
} else { throw new RuntimeException("bad"); }
}
Instruction: Produce the audit table, then the cleaned-up orchestrator.
Solution
**Audit table.** | Smell | Where | Remediation | |---|---|---| | Deep nesting (4 levels) | the `if (d != null) { if (amt != null) { if (>0) …` pyramid | Invert into guard clauses that return/throw early (Task 1). | | Acronym / single-letter soup | `proc`, `d`, `c`, `a`, `x`, `g`, `r` | Rename to `processPayment`, `request`, `currency`, `amount`, `paymentId`, `gateway`, `receipt` (Task 9). | | Boolean flag parameters × 3 | `retry, async, dryRun` → 8 combinations | Replace with an options object / enums; `proc(d, true, false, true)` is unreadable (Tasks 2, 4, 10). | | Primitive-stringly typed input | `Mappublic Receipt processPayment(PaymentRequest request, PaymentOptions options) {
validate(request); // throws a specific, typed error
PaymentId id = newPaymentId();
Gateway gateway = options.retry()
? new RetryGateway(MAX_RETRIES)
: new Gateway();
if (options.dryRun()) {
return Receipt.dryRun(id, request);
}
if (options.async()) {
executor.submit(() -> charge(gateway, request, id));
return Receipt.accepted(id, request); // explicit "accepted", not null
}
return charge(gateway, request, id);
}
private Receipt charge(Gateway gateway, PaymentRequest request, PaymentId id) {
Receipt receipt = gateway.charge(request.currency(), request.amountCents(), id);
audit(receipt); // audit no longer mutates hidden state
return receipt;
}
// typed inputs replace the Map and the boolean soup:
record PaymentRequest(Currency currency, int amountCents) {
PaymentRequest {
if (amountCents <= 0) throw new IllegalArgumentException("amount must be positive");
}
}
record PaymentOptions(boolean retry, boolean async, boolean dryRun) {}
Self-Assessment¶
Rate yourself on each capability. You "have it" when you can do it in code review, on someone else's code, without notes.
- I can flatten a 4-deep nesting pyramid into guard clauses without changing behavior, and explain why cognitive complexity dropped while cyclomatic complexity did not (Task 1).
- I can convert a long positional parameter list into a parameter/options object with defaults, and articulate the call-site readability win (Task 2).
- I can spot a "clever" one-liner that hides a bug (e.g., single-pass iterator consumed twice) and unpack it into traceable steps (Task 3).
- I can eliminate a behavior-switching boolean parameter by splitting methods or introducing an enum, and choose correctly between the two based on shared-code volume (Tasks 4, 10).
- I can convert exception-as-flow into explicit value-returning control flow while still letting genuinely exceptional failures propagate (Task 5).
- I can identify a getter with hidden side effects and apply Command-Query Separation so the name advertises mutability (Task 6).
- I can chunk a screen-tall function into a short orchestrator of named phases, each independently testable (Task 7).
- I can detect mixed abstraction levels and restore a single level per function (SLAP) by pushing details down into precisely named helpers (Task 8).
- I can de-abbreviate acronym soup and replace magic constants with named ones, changing legibility without changing computation (Task 9).
- I can judge when a dense expression is an acceptable idiom versus when it hides facts, using "chunks the reader must hold" as the criterion rather than line count (Task 11).
- I can run a full audit on an unfamiliar method, enumerate every cognitive-load smell, and sketch a clean orchestrator (Task 12).
If you ticked fewer than 8 boxes, revisit the corresponding tasks and redo them on code from your own repository — the skill transfers only when you apply it to code you did not write.
Related Topics¶
- README.md — the positive rules of this chapter (the inverse of these anti-patterns).
- junior.md — junior-level definitions of each cognitive-load anti-pattern with minimal examples.
- find-bug.md — buggy snippets where the bug hides specifically because of high cognitive load.
- optimize.md — taking already-correct code and reducing its load further.
- Refactoring section — the mechanical, behavior-preserving moves (Extract Method, Replace Method with Method Object, Introduce Parameter Object) that implement most of these fixes.
- Refactoring — Code Smells — Bloater smells (Long Method, Long Parameter List, Large Class) overlap heavily with cognitive load; practice both together.
- Design Patterns — Strategy and the options-object idiom referenced in Tasks 2 and 10.
In this topic