How we design a feature before we build it
One template, three parts: the product case, the AWS change on one diagram, and the release plan. Every engineer fills it before the first commit.
We ship to a shared staging stack many times a day, and an agent writes a lot of the code. That makes code cheap. The expensive part is a wrong decision that nobody wrote down: a table without the index we need at on-sale, a promoter who can read another promoter's roster, a backfill that runs before the code that reads it.
So every feature now starts with one document. The template lives in the repo at
docs/templates/feature-design.md. Copy the template. Fill the copy.
The filled design stays out of git.
Two versions, one rule
A small diff does not always carry a small risk. A one-line change to a refund path is riskier than a new admin page. We pick the version by what the feature touches, not by its size.
Use the full version when the feature:
- changes a payment, a refund, an order or the validity of a ticket;
- changes an auth rule, a role or a tenant boundary;
- adds or changes a public API, a webhook or an MCP tool;
- needs a data migration or a backfill;
- adds, changes or deletes infrastructure: a table, a bucket, a Lambda, a cron, a queue or an external service.
Everything else uses the light version: the sections marked (L) in the template.
Part 1: the product
We started from our own PRDs. They were good at the story, and they had four gaps. The "Mission and KPI" section had no number. Each tab repeated the same goal paragraph. The business rules were hidden in the scope list. And nothing said what we would not build.
The template fixes each one:
- Blocking questions first. Each one has an owner and a decide-by date.
- Problem and users in one paragraph, with a link to the parent goal instead of a copy of it.
- Success in two layers: acceptance (what must be true on release day) and outcome (the metric, the baseline, the target, the date and where we read it).
- Scope with MVP, later and non-goals.
- Rules, states and permissions. Each business rule is one sentence and becomes a unit test. A record that moves through states gets a state table, with what happens on a duplicate or a retry. Access gets a matrix: actor, action, resource, own data only.
- Design: a link to the prototype, or the name of the page it copies.
Part 2: the tech
One diagram, marked up
We keep one reference diagram of the platform, drawn with the official AWS icons.
For a feature, you draw only the part it touches and mark each component: + new in green, ~ changed in amber, − removed in red. Everything else stays grey. A changed table carries a note with the new attribute or index. A new data flow is a green arrow.
A reviewer sees at a glance that this feature adds a route, a table, a cron and a worker, changes one table, and removes an old import. That is the conversation we want before the code exists.
Affected surfaces
One table, one row per kind of surface: data, services, DB modules, routes, APIs and MCP tools, async jobs, infra, email. You fill only the rows that change. You name the surface and the reason; the diff will show the rest.
Failure, scaling and security
- Failure handling: the idempotency key, who retries, how we find and repair a wrong record, and the alarm that tells us.
- Scaling and cost: even the light version answers one question — does the load or the cost change? Only a "yes" unlocks the rest: volume at on-sale peak, each query and its index, hot partitions, Lambda limits, external rate limits, cache tier, monthly cost. We do not ask anyone to invent numbers for a settings page.
- Security: the tenant boundary, the Zod schema at each input, the personal data and its retention, new secrets, public routes and webhook signatures, abuse limits.
Part 3: the release
Shipping is a plan, not a merge:
- Shape: one release, or staged steps.
- Gate: a flag, a role or an allowlist, and who sees it first.
- Order: for example, deploy the code, run the backfill, then switch the read.
- Old and new together: can the old code read the new data, and the reverse?
- Point of no return: after which step a rollback needs a forward fix.
- Rollback trigger: the signal, and the steps.
- Operations: the check after release, the owner on the day, how we repair.
- Communication: who must know (promoters, ops, support, the CEO), what, when and on which channel. An internal fix needs none.
Why we keep it short
The template is mostly tables, and you delete what does not apply. The aim is one short conversation before the code exists, about the index we need at on-sale, the tenant boundary and the order of the backfill. Those mistakes cost the most to fix after release.