Architecture & system design

Decision records and trade-offs

Writing architecture decision records, running lightweight RFCs, evaluating libraries and services, build versus buy, reversible versus one-way decisions, planning migrations as strangler moves, managing technical debt with a budget, and communicating trade-offs to product and leadership.

Senior engineers are hired for decisions as much as for code: which library, whether to rewrite, when to say no. Interviewers probe this with "tell me about a technical decision you made and how it turned out" and "how would you evaluate X". Good answers show a process that produces defensible, documented, reversible decisions and communicates their costs honestly.

Know

  • Architecture Decision Records: one short document per significant decision, in the repository, numbered and immutable once accepted: context (the forces: requirements, constraints, options considered), decision (what and why), consequences (what becomes easier, harder, what we are committing to), status (proposed, accepted, superseded by). A new decision supersedes rather than edits. ADRs answer "why is it like this" a year later and stop relitigating.
  • Lightweight RFCs for decisions that affect several people: a one-page proposal with the problem, options, recommendation and open questions; a comment window; a named decider; a written outcome. The senior craft section covers the team process; the point here is that architecture decisions have a path.
  • Evaluating a library or service: fit for the actual requirement; maintenance signals (release cadence, open issues age, bus factor, funding); bundle size and SSR compatibility (ESM, no window at import, works in Nitro or at the edge); TypeScript quality; accessibility if UI; licence and supply-chain posture; community and documentation; exit cost (how hard is it to remove); a spike with a time box before committing.
  • Build versus buy: buy when the capability is undifferentiated (auth UI, search engine, error tracking, payments) and the vendor's failure modes are acceptable; build when it is the product's differentiator or when integration costs exceed building; in between, build on a maintained base (Nuxt UI over a bespoke design system, Better Auth over hand-rolled flows).
  • Reversible versus one-way decisions: a UI library or a caching strategy can be changed later; a database engine, a public API contract, a data model exposed to customers, or a multi-tenant partitioning scheme is a one-way door. Spend deliberation time in proportion; make reversible decisions quickly and one-way decisions with ADRs, prototypes and review.
  • Migrations as strangler moves: put the new system in front or beside the old, route slices to it behind flags, measure, expand, and remove the old when nothing is left. Examples: Vue 2 → 3 with the compat build route by route; Options → Composition API component by component; Nuxt 3 → 4 with codemods and compatibilityVersion; REST → typed procedures by adding oRPC alongside; a monolith frontend → Nuxt by proxying routes. Rewrites lose the accumulated behaviour nobody documented; stranglers keep it.
  • Technical debt as a portfolio: a visible register with cost of delay and effort, a standing budget (a fixed share of each iteration) rather than "a debt sprint someday", debt tied to business risk (security, incident frequency, hiring), and interest paid down where change is frequent.
  • Observability and security as design inputs: decisions include how success and failure will be measured and what the threat model is; "we will know it works because…" is part of the record.
  • Communicating trade-offs: to product and leadership in their currency, time, risk, money and user impact, with options rather than a single ask ("we can ship in two weeks with a per-tenant cache leak risk we mitigate by X, or in four weeks with the proper key"), and a clear recommendation. Saying no is offering the alternative that meets the goal.
  • Reviewing outcomes: revisit ADRs when consequences materialise; record what turned out wrong. A senior's credibility comes from decisions they changed as much as from decisions they made.

How it works

An ADR for a decision from this section:

docs/adr/0007-rendering-strategy-catalog.md
# 7. Catalogue pages use swr (300 s) instead of prerendering or per-request SSR

Status: accepted (2026-09-21) · Supersedes: 3 (prerender whole catalogue)

## Context
50k products, prices change hourly, SEO required. Prerendering the catalogue takes 40 minutes per deploy and cannot follow price changes;
per-request SSR costs ~120 ms CPU per page and the origin saturates at peak. Personalised parts (cart badge, stock) are small.

## Options
1. Prerender everything on each deploy (current) — SEO fine, prices stale up to a day, slow deploys.
2. SSR per request — fresh, expensive, origin-bound.
3. swr 300 s via route rules, personalised parts client-side — fresh within 5 min, one render per URL per interval.
4. isr at the CDN — same as 3 with CDN regeneration; not supported by our current host.

## Decision
Option 3. `/c/**` and `/p/**` use `swr: 300` with cache keys including locale and currency; cart and stock load client-side;
checkout revalidates prices server-side. CMS and price webhooks clear affected keys.

## Consequences
+ Deploys no longer rebuild the catalogue; origin load drops to one render per URL per 5 minutes.
+ SEO unchanged (HTML served from cache with correct status).
− Prices may be up to 5 minutes stale on listing pages (mitigated at checkout).
− Cache storage must be shared (Redis) across instances; invalidation must be exercised in tests.
We will know it works by: origin CPU p95 under 40%, catalogue LCP p75 under 2.0 s, zero price-mismatch incidents at checkout.

A library evaluation checklist, as a scoring table:

Candidate: TanStack Query vs Pinia Colada vs built-in useAsyncData (for the dashboard app)
Criterion               Weight  TanStack  Colada  Built-in
Fit (SWR, mutations,     5        5         4        2
  infinite, focus refetch)
SSR/Nuxt integration     4        4         5        5
Bundle size              3        3         4        5
Maintenance/bus factor   4        5         3        5
TypeScript quality       3        5         4        4
Exit cost                3        3         3        5
Weighted total                    92        86       94   -> built-in for the marketing app; TanStack for the dashboard

A migration plan as a strangler, with measurable gates:

Vue 2 monolith -> Nuxt 4 (12 months, 6 engineers)
Phase 0  Safety net: E2E on the 8 critical flows; error tracking with releases; performance baselines.
Phase 1  Proxy: Nuxt in front, routeRules proxy everything to the old app; deploy; nothing changes for users.
Phase 2  Slice 1 (marketing pages): build in Nuxt, flip route rules per path; gate: LCP p75 ≤ old, error rate flat.
Phase 3  Shared kernel: design tokens + primitives as a layer; auth session bridge (shared cookie).
Phase 4  Slices 2..n by business value and coupling; each behind a flag with a rollback = route rule revert.
Phase 5  Remove the proxy and the old app; archive; ADR closing the migration with lessons.
Gates: every phase has a metric and a rollback; no phase longer than 6 weeks without a user-visible slice shipped.
Gotcha· The undocumented decision

Six months later nobody remembers why the cache is 300 seconds or why the account area is an SPA, and the team relitigates it in every planning meeting or, worse, changes it without knowing the consequence it prevented. A ten-line ADR costs less than one such meeting.

Gotcha· Rewrite as the default plan

"We will rewrite it properly" discards behaviour nobody documented, freezes feature work for months, and ships a version one with fewer capabilities than the legacy app. Stranglers ship value from the first slice and keep the rollback trivial.

Exercise

Exercise
  • Write an ADR for a decision you made in the last year, including the options you rejected and how you would know it worked; ask a colleague whether they could have made the same call from the document alone.
  • Score two libraries you are considering with the weighted table; notice where the weights, not the scores, decide.
  • Draft a strangler plan for a migration your team keeps postponing, with gates and rollbacks per phase, in one page.

Be able to say

Be able to say· How do you make and document technical decisions?

"By separating reversible from one-way decisions. Reversible ones, a UI library, a caching TTL, I make quickly with a time-boxed spike and record in a short ADR: context and the options considered, the decision, the consequences including what we are giving up, and how we will know it worked. One-way decisions, a database, a public API contract, a tenancy model, get a one-page RFC with a comment window and a named decider before the ADR. For libraries I score fit, maintenance signals, bundle size and SSR compatibility, TypeScript quality, accessibility, licence and exit cost. And I revisit records when consequences show up; changing a decision on evidence is part of the process, not a failure of it."

Be able to say· How would you migrate a large legacy frontend to Nuxt?

"As a strangler, never a rewrite. First the safety net: end-to-end tests on the critical flows, error tracking with releases, performance baselines. Then Nuxt goes in front of the old app and proxies everything, so deploying it changes nothing. Then slices by business value and low coupling, marketing pages first: build the slice in Nuxt, flip its route rules, gate on metrics, keep the rollback as a one-line revert. In parallel the shared kernel becomes a layer: tokens, primitives, an auth bridge sharing the session cookie. Every phase ships something visible within weeks; when the last slice moves, the proxy and the old app are removed and the ADR closes with the lessons. A rewrite would have frozen features for a year and lost the behaviour nobody wrote down."

Your notes

Open in Notes

Nothing yet. Notes filed here show up on this page and the section overview.