Crux Read real diffs and a CI config, predict the trunk-based behaviour, and pick the highest-leverage fix — flag wrapping, branch by abstraction, and the green gate.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at senior altitude — in orbit
◷ 14 min
Trunk-based discipline shows up in the diff and the CI config, not the manifesto. Read each snippet, predict how it behaves on a shared trunk, and choose the fix a senior engineer would make first.
Goal
Practise reading the artefacts that decide whether a team is actually trunk-based: how unfinished work is wrapped to ship dark, how a big change rides trunk via abstraction, and how the gate is configured to keep trunk green.
Snippet 1 — shipping unfinished work to trunk
// New checkout flow isn't finished, but it must merge to trunk today.export async function handleCheckout(cart: Cart, user: User) { if (flags.isEnabled("checkout-v2", { userId: user.id })) { return newCheckout(cart, user); // half-built, still missing tax calc } return legacyCheckout(cart, user); // current production path}
Quiz
Completed
The flag 'checkout-v2' defaults to off in production. With this code merged to trunk daily, what is true?
Heads-up Deployed is not released. The flag is off, so users hit legacyCheckout; the new path is present but dark. That's exactly how trunk-based ships incomplete work safely.
Heads-up The whole point of the flag is that newCheckout can be incomplete. It merges daily behind the off flag and is flipped on only when done — release is decoupled from deploy.
Heads-up The flag replaces the long-lived branch. Putting the conditional on trunk is what lets the work integrate daily without accumulating drift.
Snippet 2 — a six-week ORM swap on trunk
interface UserStore { // the abstraction find(id: string): Promise<User>; save(u: User): Promise<void>;}class LegacyOrmStore implements UserStore { /* current, in use */ }class NewOrmStore implements UserStore { /* built incrementally, dark */ }// wired once, flipped at the boundary when NewOrmStore is readyexport const userStore: UserStore = flags.isEnabled("orm-v2") ? new NewOrmStore() : new LegacyOrmStore();
Quiz
Completed
Why is this branch-by-abstraction shape the trunk-based answer to a migration too big to ship in a day?
Heads-up The point is the opposite: NewOrmStore is built on trunk behind the abstraction, never on a long-lived branch. The abstraction is what removes the need for the branch.
Heads-up A direct rewrite forces either a long-lived branch or a broken trunk. The abstraction is what keeps trunk releasable throughout and lets you flip or revert at one boundary.
Heads-up After the new path is verified, you delete the old implementation and then the now-redundant abstraction — leaving it is its own form of leftover scaffolding.
Snippet 3 — the CI gate config
# .ci/trunk-gate.yml — runs on every PRon: pull_requestjobs: gate: steps: - run: make unit-tests # ~2 min, blocks merge - run: make lint typecheck # ~1 min, blocks merge e2e: steps: - run: make e2e-suite # ~40 min if: github.event_name == 'schedule' # nightly only, never blocks
Quiz
Completed
Reading this gate, which statement is the correct senior assessment?
Heads-up A 40-min blocking suite would push developers to batch changes, lengthening branches and reabsorbing drift. Tiering fast checks pre-merge and slow ones post-merge/nightly is the standard way to keep the gate fast.
Heads-up Relying on memory isn't a gate. The design choice is deliberate: block on fast checks, run slow ones nightly, and treat a rare nightly failure with stop-the-line plus bisect.
Heads-up Fast static checks blocking the merge is exactly what keeps trunk green cheaply. The risk to manage is slowness, not strictness — and these checks are seconds-to-minutes.
Snippet 4 — a stale flag left behind
// shipped 14 months ago, "search-v2" has been at 100% the entire timefunction rankResults(q: Query, results: Result[]) { if (flags.isEnabled("search-v2")) { return rankV2(q, results); // the live path } return rankV1(q, results); // never runs, never updated since launch}
Quiz
Completed
search-v2 has been at 100% for 14 months. What is the highest-leverage action, and why?
Heads-up That's type laundering — relabeling leftover release scaffolding as a permanent ops flag keeps the rotting old path alive. If a kill switch is genuinely needed, design one deliberately.
Heads-up Deferred flag cleanup is exactly how teams reach 400 stale flags. Removal has to be the closing step of the rollout, part of 'done,' not a follow-up that gets deprioritized.
Heads-up It isn't harmless: rankV1 is still reachable (a config typo can route to it), unmaintained, and one more boolean in the combinatorial config space. A release flag at 100% should be deleted.
Recap
Trunk-based discipline is legible in the artefacts. A flag conditional on trunk lets unfinished work ship dark so it integrates daily with no drift; an abstraction interface lets a six-week migration ride trunk as small green commits with a single flip-and-delete; a tiered CI gate stays fast by blocking on unit/lint/typecheck and running slow e2e off the critical path; and a release flag still on at a stable 100% is a long-lived branch hiding in an if that you delete — flag and dead path together — as the closing step of the rollout.