Skip to content
C.W.K.
Stream
Lesson 02 of 05 · published

Every Shelf Is a Projection, and Projections Are Disposable

~12 min · architecture, projections, rendering, cqrs

Level 0Unsorted
0 XP0/36 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

What a Shelf Actually Is

A shelf looks like a container. It is not. "For You", "Queue", "Archived", "Korea" — none of these hold articles. Each is a function that takes the store plus the projected reading state and returns an ordered list, computed the moment someone asks. Nothing is filed into a shelf and nothing is removed from one; the inputs change and the function returns something different.

Getting this straight early is what keeps the design coherent later, because it settles a question that otherwise gets answered differently in every feature: where does a rule live? If "disliked articles do not appear here" is a property of the shelf function, it applies consistently and can be changed in one place. If it is implemented by deleting rows, it has become irreversible, and the next feature that wants disliked articles — a settings screen showing what you muted, say — has nothing left to show.

Render-Time Rules Compose

Once shelves are functions, the interesting behaviors become composable steps in a pipeline rather than special cases. A shelf query over-fetches from the store, then passes the rows through a sequence: collapse repeats of the same story, drop muted terms, promote pictures into the visible region, trim to the requested size. Each step is independently testable and independently removable.

The alternative — baking each of these into SQL, or worse into the ingest path — buys a little speed and costs you the ability to reason about any of them. A rule applied at ingest is a rule you cannot turn off for one shelf, cannot explain to the reader, and cannot change your mind about without a migration.

Say What You Did

A projection that quietly reorders or removes things is a black box wearing a list's clothes. Every step that changes what the reader sees should report a count alongside the rows: this many repeats collapsed, this many muted, this many promoted. The client can then show it, and the reader can tell the difference between "there is nothing here" and "there is plenty here and I filtered it."

The Freedom You Get Back

The payoff for all this discipline is that projections become cheap to be wrong about. A ranking function that turns out to over-weight something is a bug you fix and recompute — no migration, no backfill, no data loss, because nothing about the fix touches the log. That is the actual reason to keep truth and presentation separate: not purity, but the ability to change your mind about presentation as often as you need to.

If a rule can be expressed at render time, it must be. Every rule you push earlier — into SQL, into ingest, into a stored flag — trades away reversibility for a small amount of speed you almost certainly do not need on a personal-scale corpus.

Code

A shelf as a composed pipeline that reports what each step did·python
def shelf(con, tab_id: int, limit: int) -> dict:
    """A shelf is a FUNCTION of (store, projected state), not a table.
    Over-fetch, then compose the render-time rules in order."""
    rows = card_rows(con, SHELF_SQL, (tab_id, limit * OVERFETCH))

    rows, collapsed = dedupe_stories(rows)     # one story, many URLs
    rows, muted     = apply_mutes(con, rows)   # reader's negative signals
    rows, promoted  = apply_image_floor(rows, limit)

    return {
        "articles": rows[:limit],
        # Invariant: a step that changes what is shown SAYS SO. A shelf
        # that silently reorders itself is a black box, and the reader
        # cannot tell 'nothing here' from 'plenty here, filtered'.
        "collapsed": collapsed,
        "muted": muted,
        "promoted": promoted,
        # Never a count(*): a projection rebuilt per render does not
        # know its own total. 'Has more' is inferred from a short page.
        "has_more": len(rows) > limit,
    }

External links

Exercise

Take a list view in something you have built and classify every rule that decides what appears in it: applied at ingest, in SQL, or at render. For each rule earlier than render, work out what it would cost to make it a render-time step, and whether the reason it is early is performance you have measured or performance you assumed.
Hint
Look for rules implemented as deletions or as boolean columns set once. Those are the irreversible ones. A rule expressed as a WHERE clause is at least still reversible — you can change the clause — but it still cannot report how many rows it removed, which is the property the reader actually needs.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.