Whetstone.
Decode your feedbackDecode: layering, boundaries, and naming
Module 12 · Lesson 314 min

Decode: layering, boundaries, and naming

Three of Rai’s reviewers pushed the same underlying idea from different angles: logic belongs to the layer that owns it, and decisions get made at the boundary. This lesson decodes all three, because they’re one principle.

The comment (Nisha Nambissan, PR #491, on a frontend retry):

“the pattern of retrying on the browser side looks like an antipattern. This should not have been a frontend concern in the first place. We have surfaced it backwards and made it a client concern, when it should have stayed in the backend and been addressed there.”

And (Nick Ferrell, PR #355, on a nullable date threaded downstream):

“Why allow null at all? Doesn’t it just use the current date anywhere? I would think about making this explicit upstream rather than buried downstream.”

And (Nick, same PR, on the query store):

“nit: the querystore should NOT be making this business decision.”

What they meant. A resilience concern (retrying a flaky dependency) is owned by the backend, which makes the call; pushing it to the browser inverts the layering and, worse, stacks client retries onto a cold-start timeout. A business default (what “no date” means) is owned by the boundary, the endpoint; defaulting it with ?? today deep in the query store buries a product decision in the persistence layer, where it’s invisible and untestable. Both are the same rule: push each decision to the layer that owns it, and make it explicit at the edge.

Decide at the boundary (Rai's resolution of Nick's note)
// What Nick flagged: a nullable date threaded down, defaulted deep inside.
public Task<FlightsView> Execute(DateOnly? day) { var d = day ?? Today; /* ... */ }

// The fix: the endpoint resolves the default; Execute takes a non-null DateOnly.
public Task<FlightsView> Execute(DateOnly day, CancellationToken ct) { /* ... */ }
// endpoint: useCase.Execute(day ?? DateOnly.FromDateTime(clock.UtcNow), ct)

And the boundary-validation flavour of the same idea, from Nick on PR #397:

“Data should be cleaned/valid before being sent/used in the domain.”

Rai’s fix: “the query store drops (and logs) any row without a unique_flight_key, so the aggregate receives a guaranteed-present, valid id.” Clean at the boundary so the domain never re-checks; its invariants are guaranteed by the fact that nothing invalid can reach it.

Practice

Try it yourself

Recall

Decide at the boundary

State Nick's rule in your own words.

Nick asked, of a nullable date threaded deep into a query store, "Why allow null at all? ... make this explicit upstream rather than buried downstream." What's the general rule, and how did Rai resolve it?

Reveal answer

A business default (what 'no date supplied' means) must be resolved at the boundary, the endpoint, not defaulted with ?? today deep in the persistence layer. Rai's fix: Execute now takes a non-null DateOnly, and the endpoint resolves the default before calling, so the operational day is decided at the edge rather than threaded down as a nullable. Low layers shouldn't make product decisions.

Quiz

Where the retry belongs

Nisha wrote that browser-side retrying "should not have been a frontend concern in the first place ... it should have stayed in the backend." What layering principle is that?

  1. AThe frontend should never be allowed to call the backend service directly
  2. BRetries are simply always wrong, everywhere, in every case
  3. CThe database should be the one to do the retry
  4. DRetry belongs to the layer that owns the operation; pushing it to the client inverts the layering
Show answer

Correct answer: D — Retry belongs to the layer that owns the operation; pushing it to the client inverts the layering

Retry is a backend responsibility because the backend owns the call to the flaky dependency. Surfacing it to the browser inverts the layering and piles client retries onto a cold-start timeout, making it worse. Each layer keeps the concerns it owns.

Sign in to track your progress →