From idea to one-page spec
Every app starts as a sentence in someone's head, and most of the wasted weeks in software come from building that sentence before anyone has pinned it down. This lesson turns an idea into one page that says who the app is for, what problem it solves, how you will know it worked, what it will not do, and the smallest version worth building first. That page drives every later stage of this course.
By the end you can
- Turn an idea into a problem statement with a named user and real evidence
- Write goals with a baseline, a target and a date, plus one guardrail
- Write non-goals that stop work, each with a reason
- Cut a wish list down to the smallest useful version that fits a time budget
- Keep a one-page spec in your repository, with acceptance checks and a small script that checks it
The everyday picture
Picture someone opening a food stall near a busy bus station. She could start with a menu of twenty dishes, card payments, delivery and a loyalty card. Or she could write one card first. Who will eat here? Office workers with twenty minutes for lunch. What do they need? A hot meal, fast, for a fair price. How will she know it works? She sells 80 plates by 2 pm on most days of the first week. What will she not do yet? No delivery and no card machine, because she cannot afford either until she knows people come back.
Her smallest useful version is one good rice dish, one drink and a queue that moves. It is small, and it is complete: a customer can arrive hungry and leave fed. After a week she knows more than any plan could tell her, and she adds the second dish because people asked for it. A one-page spec is that card, written for an app.
From an idea to a problem with evidence
Ideas usually arrive as solutions. "Let's build a reorder app." "Add AI suggestions." A solution hides the question that matters: what is going wrong today, for whom, and how do we know? If you start building the solution, you cannot tell later whether it helped, because nobody wrote down what "helped" means.
So the first part of the page is a problem statement. Write it in four parts:
- Who: one kind of person, named by role. "Owners of small partner shops", not "users".
- What they are trying to do: the job, in their words. "Order next week's stock."
- What goes wrong today: the delay, the mistake or the cost. "They phone the same order in every week, and some phone orders arrive wrong."
- How we know: at least one number and one quote. Numbers show size; a quote shows the reason behind the number.
Then test the statement. Could a different solution fix the same problem, such as a printed order form or a saved order the sales rep keeps? If yes, good: you have described a problem, and your app is one of several ways to solve it. If the statement only makes sense with your app in it ("shops lack a reorder app"), you wrote the solution twice. Problems before solutions has more ways to find the problem behind a request.
Next, the users. Name the primary user, the one person whose job the first version must make easier. Then list who else is touched: people whose work changes, the person who decides, and the team who will run or rebuild it later. For each primary user, write the context that changes your design: the device, the network, the time of day and the pressure they are under. "Orders after closing time, on a cheap Android phone, on patchy mobile data" will shape your screens, your timeouts and your tests far more than an age range would.
Evidence does not need a research project. Order data you already have, plus five short conversations with real users, is usually enough to write this section with confidence. User interviews that don't lie shows how to ask about what people did last week instead of what they might like.
Quick check
Which line is a problem statement, and not a solution in disguise?
The second names who, what goes wrong and how big it is, and several solutions could fix it. The first describes the app you want to build, so it cannot tell you whether the app helped. The third is a hope about feelings with no evidence and nothing to measure.
Goals you can measure, non-goals that stop work
A goal describes a change in what people do, with numbers. Each goal needs five things: a metric, a baseline (where it is today), a target, a date, and where the number comes from. If any one is missing, the goal will start an argument at the end instead of settling one.
For a small first version, pick a metric that can move in weeks with a handful of users. A company-wide number, such as the share of all partner orders placed in the app, matters, but a pilot with 20 shops barely moves it, so you would learn nothing. Choose a number about the pilot group itself: "the share of pilot shops' orders placed in the new app, from 0% to 40% by the end of week 4, counted in the order table". OKRs and outcome goals goes deeper on picking outcome metrics.
Add one guardrail: something that must not get worse while you chase the goal. A reorder app that gets more orders in but doubles the number of wrong deliveries has failed. "Corrections after delivery stay at 1 in 12 or better" protects the people at the other end of your app.
Then write the non-goals. A non-goal is something a reasonable person could expect the app to do, which you have decided it will not do yet, with the reason. Non-goals do two jobs. They stop a stakeholder from waiting for a feature that is not coming, and they stop you, the builder, from adding it on a quiet afternoon. "The app will not be slow" is not a non-goal; nobody planned a slow app. "No payments in the app, because invoices stay on today's process" is one: people will ask about it, and now the answer is written down.
A written reason also lets you reopen a non-goal when the reason changes. Writing specs people read covers goals, non-goals and open questions for larger specs.
Quick check
A 20-shop pilot will run for four weeks. Which goal will tell you, at the end, whether the first version worked?
The third has a metric, a baseline, a target, a date and a source, and 20 shops can move it. The first is the right long-term goal, but 20 shops out of about 1,800 hardly change it in four weeks. The second cannot be measured, and the fourth is a delivery date, an output, which says nothing about whether anyone used the app.
The smallest useful version
With the problem and goals written, list what the first version will do. This is where most beginner projects go wrong. The list grows to fifteen features, and when the time runs out nobody can use the app for anything.
The fix is a method in three steps.
- Set the time budget first. Decide how much time the problem is worth before you list features: for example, three weeks for one developer. Basecamp's Shape Up method calls this the appetite. An estimate asks how long the features will take; an appetite asks what you can build in the time the problem deserves. The time stays fixed and the scope changes to fit.
- Write the user's journey as steps. For a reorder app: sign in, see last orders, repeat one with changes, see its status, see a weekly summary. These steps are the backbone. Every step the user needs to finish the job must exist in the first version, or the job cannot be done at all.
- For each step, pick the cheapest version that works. Sign-in can be a simple pilot login before it is a full account system. "Last orders" can be the newest five, with no search or filters. The summary can be one screen of numbers. Each step is basic, and together they let a real user finish the real job.
The result is a thin vertical slice: a basic version of every step, built through every layer of the app, from screen to API to database. The opposite is building layer by layer, or step by step to a polished finish. Both feel productive, and both can leave you with nothing a user can try. The first diagram shows the difference.
People often call this a minimum viable product (MVP). The word "minimum" tempts teams to ship something too thin to teach them anything, so this course says smallest useful version: small enough to fit the appetite, useful enough that a real user does the real job with it, and complete enough that you can measure the goal.
Everything you cut goes into one of two lists. Later is for ideas you might build after you learn from the pilot. Never in this version is your non-goals. Writing cut ideas down makes cutting easier, because nothing is lost.
Quick check
You have three weeks. Which plan gives a shop owner something to try at the end?
Only the thin slice lets a user finish the job, so only it can tell you whether the goal moves. A polished sign-in leads to an app with nothing behind the door, and building the back end first pushes all the risk about the screens into the last few days, when there is no time left to change them.
From spec to plan: acceptance checks, risks and open questions
A list of features says what the app has. It does not say when each feature is done. For that, give every item in the smallest version one to three acceptance checks, written as Given, When, Then:
- Given a shop's last order had 18 lines, when the owner taps Repeat, then a draft opens with the same 18 lines and quantities.
- Given an item is no longer sold, when the owner repeats an order with it, then that line shows as unavailable and is left out.
- Given the network drops after the owner taps Confirm, when the app retries, then exactly one order is created.
The first tells the designer what the repeat screen must show. The second tells you the data model needs to know whether an item is still sold. The third is a hard rule for the API and the app on patchy mobile data, and it will become a test. Good acceptance checks pick out the edge cases before they become bugs. User stories and acceptance criteria shows how to find more of them on purpose.
Next, write down the riskiest assumption: the belief that, if it is false, makes the whole app pointless. For a reorder app it might be "owners will trust a repeat order without calling their rep to check it". Say how you will test it cheaply before building, for example with a paper version of the flow and three shop owners. Testing ideas cheaply has four ways to do this, and the next lesson, Screens and flows, does it for this app.
Finally, list the open questions, each with an owner and a due date. "Does a repeated order use today's prices or the old ones? Owner: product manager, due 7 November." A question with an owner gets answered. A question without one becomes a guess in the code, and someone finds it in production.
Keep the whole thing to one page, roughly 400 to 800 words. Put it in your repository as docs/spec.md, next to the code, so it changes through pull requests and keeps its history (Pull requests and code review). When a decision changes the spec, change the spec in the same pull request as the code. A spec that drifts away from the app is worse than none, because people trust it.
Build it
Write the one-page spec for the reorder app, keep it in the project's repository, and add a small Node.js script that checks the page has every part. You need Git and Node.js 18 or newer (see Node.js and npm).
-
Start the project repository
The spec is the first file of the project you build through this course. Create the folder, start Git, and make a
docsfolder for the spec and ascriptsfolder for the checker.Terminalmkdir reorder-app cd reorder-app git init mkdir docs scripts -
Work out the numbers before you write a goal
Goals come from arithmetic on the data you have. Write it out once, so anyone can check it. These are the reorder app's numbers, all approximate:
OutputPartner orders a week about 4,000 placed in the Partner app today about 62% (company target 75%) placed by phone, to about 40 reps about 1,500 (about 37%) Phone orders corrected after delivery 1 in 12 -> about 125 a week Pilot: 20 shops that phone in at least 1 order a week, for 4 weeks orders expected 20 x 4 = about 80 goal: 40% of them in the reorder app about 32 orders guardrail: corrections 1 in 12 or better -> at most about 7 of 80 Appetite: 3 weeks for 1 developerThe pilot goal is small on purpose. Twenty shops cannot move the company's 62% much, but 32 repeat orders out of 80 will tell you clearly whether owners use the app instead of the phone.
-
Write the first half: problem, users, goals, non-goals
Save this as
docs/spec.md. Each section is a##heading, and every goal, non-goal and question is a line starting with-, so the script in step 5 can read it.Markdown# Reorder app for partner shops: one-page spec Owner: Imran (product). Builder: you. Status: draft 1, 3 November. ## Problem Partner shops reorder nearly the same items every week, yet about 1,500 of the roughly 4,000 partner orders a week still go by phone to about 40 sales reps. About 1 phone order in 12 is corrected after delivery: roughly 125 a week. Evidence: 8 weeks of order data; calls with 5 shop owners. "I order the same things every Monday. Typing it all again takes too long." (Ravi, corner shop owner) ## Users - Primary: shop owners like Ravi, ordering after closing time, on a cheap Android phone and patchy mobile data - Also affected: sales reps (fewer calls) and DC pickers (same orders, new source) - Decides: Imran. Builds the main Partner app later: Lagoon Digital ## Goals - Pilot shops place 40% of their orders in the reorder app: baseline 0%, target 40%, by the end of week 4 - Guardrail: corrections after delivery, baseline 1 in 12, target no worse, by the end of week 4 ## Non-goals - No payments in the app, because invoices stay on today's process - No search or browsing of new products, because repeats are the problem we saw - No ordering while offline, because a queued order can be sent twice -
Write the second half: smallest version, checks, risks, questions
Add the rest to the same file. Five items in the smallest version, each step of the journey in its most basic form. The Later list keeps every cut idea visible.
Markdown## Smallest useful version - Sign in as a pilot shop - See the last 5 orders, newest first - Repeat one order: change quantities, remove lines, confirm - See each order's status, from placed to delivered - A weekly summary: number of orders and total spent ## Acceptance checks - Given Ravi's last order had 18 lines, when he taps Repeat, then a draft opens with the same 18 lines and quantities - Given an item is no longer sold, when he repeats an order with it, then that line shows as unavailable and is left out - Given the network drops after he taps Confirm, when the app retries, then exactly one order is created ## Risks - Riskiest assumption: owners trust a repeat without calling their rep. Test it with a paper flow and 3 shops before building ## Open questions - Does a repeat use today's prices or the old ones? Owner: Imran, due 7 November - Can two people sign in for one shop? Owner: Imran, due 10 November ## Later - Suggested items, promotions, push notifications, drafts saved offline -
Add a script that checks the spec
A spec gets weaker as people edit it: a goal loses its date, a non-goal loses its reason. This script catches that. It uses only Node's built-in
fsmodule, so there is nothing to install. Save it asscripts/check-spec.mjs.JavaScript// Run it with: node scripts/check-spec.mjs docs/spec.md import { readFileSync } from 'node:fs'; const file = process.argv[2] ?? 'docs/spec.md'; const text = readFileSync(file, 'utf8'); const problems = []; // 1. Every required section is there as a "## " heading const required = ['Problem', 'Users', 'Goals', 'Non-goals', 'Smallest useful version', 'Acceptance checks', 'Risks', 'Open questions']; const headings = [...text.matchAll(/^## (.+)$/gm)].map((m) => m[1].trim()); for (const name of required) { if (!headings.some((h) => h.startsWith(name))) problems.push(`missing section: ${name}`); } // 2. The "- " lines of one section, up to the next heading function items(name) { const lines = text.split('\n'); const start = lines.findIndex((l) => l.startsWith(`## ${name}`)); if (start === -1) return []; const rest = lines.slice(start + 1); const end = rest.findIndex((l) => l.startsWith('## ')); return (end === -1 ? rest : rest.slice(0, end)).filter((l) => l.startsWith('- ')); } // 3. Goals need numbers, non-goals a reason, questions an owner for (const g of items('Goals')) { if (!/baseline/i.test(g) || !/target/i.test(g) || !/\bby\b/i.test(g)) problems.push(`goal needs baseline, target and date: ${g}`); } for (const n of items('Non-goals')) { if (!/because/i.test(n)) problems.push(`non-goal without a reason: ${n}`); } for (const q of items('Open questions')) { if (!/owner:/i.test(q)) problems.push(`question without an owner: ${q}`); } if (items('Smallest useful version').length > 7) problems.push('smallest version has more than 7 items'); // 4. One page is roughly 800 words at most const words = text.split(/\s+/).filter(Boolean).length; if (words > 800) problems.push(`spec is ${words} words; aim for 800 or fewer`); console.log(problems.length ? problems.join('\n') : `OK: ${words} words, every section present`); process.exitCode = problems.length ? 1 : 0; -
Run it, break it, and commit
Run the check on the full spec. Then delete the reason from the payments non-goal and the owner from the second question, and run it again to see it fail. A non-zero exit code means you can run the same script in CI later (Continuous delivery).
Terminalnode scripts/check-spec.mjs docs/spec.mdOutputOK: 407 words, every section presentOutputnon-goal without a reason: - No payments in the app question without an owner: - Can two people sign in for one shop? due 10 NovemberPut the reason and the owner back, then commit the spec and the script together:
Terminalgit add docs/spec.md scripts/check-spec.mjs git commit -m "docs: one-page spec for the reorder app"The script checks shape, not judgement. It cannot tell a good goal from a silly one with the right words in it. That part is your job, and the job of the person who reviews the spec with you.
Bring it to your team
Before anyone builds, review the page together with these questions.
- Who exactly is the primary user, and what did we see or hear from at least five of them?
- If we hit the goal, what number changes, from what, to what, by when, and where do we read it?
- What must not get worse while we chase it?
- Which features will people expect that we are not building, and have we told them why?
- Can a user finish the whole job with the smallest version, and does it fit the time we agreed?
- Which open question blocks the most work, and who answers it by when?
Red flags
- A problem statement that names the app ("shops lack a reorder app").
- Goals with no baseline, or goals that are a ship date.
- No non-goals, or non-goals like "it will not be slow".
- A first version with more than about seven items, or one that cannot be used end to end.
- A spec that lives in someone's chat messages and no longer matches the app.
Practice
Find the gaps in a spec
Write specGaps(spec), a function that reviews a spec stored as an object and returns a list of the gaps it finds. The spec looks like this: { problem, evidence: [...], goals: [{ metric, baseline, target, by }], nonGoals: [{ item, because }], now: [...], openQuestions: [{ question, owner }] }, and any field may be missing. Return these strings, in this order, for each rule that fails:
'no problem statement':problemis missing or only spaces'no evidence':evidenceis missing or empty'goal without numbers': there are no goals, or a goal has nobaseline,targetorby(a baseline of0is a real number)'non-goal without a reason': there are no non-goals, or one has nobecause'no smallest version'whennowis missing or empty, or'too big'when it has more than 7 items'question without an owner': an open question has noowner(no questions at all is fine)
function specGaps(spec) {
const gaps = [];
if (!spec.problem) gaps.push('no problem statement');
// add the other rules, in the order listed above
return gaps;
}
const good = {
problem: 'Shops phone in the same order every week; 1 in 12 phone orders is corrected.',
evidence: ['8 weeks of order data', '5 calls with shop owners'],
goals: [{ metric: 'share of pilot orders in the app', baseline: 0, target: 40, by: 'week 4' }],
nonGoals: [{ item: 'payments', because: 'invoices stay on the current process' }],
now: ['sign in', 'last orders', 'repeat with changes', 'status', 'weekly summary'],
openQuestions: [{ question: 'today\'s prices or old ones?', owner: 'Imran' }],
};
test('a complete spec has no gaps (a baseline of 0 counts as a number)', () => {
expect(specGaps(good)).toEqual([]);
});
test('a first draft shows every gap, in the listed order', () => {
const draft = {
problem: 'Shops need a better app',
evidence: [],
goals: [{ metric: 'happier shops' }],
nonGoals: [{ item: 'payments' }],
now: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'],
openQuestions: [{ question: 'prices?' }],
};
expect(specGaps(draft)).toEqual(['no evidence', 'goal without numbers', 'non-goal without a reason', 'too big', 'question without an owner']);
});
test('missing fields count as gaps; no open questions is fine', () => {
expect(specGaps({})).toEqual(['no problem statement', 'no evidence', 'goal without numbers', 'non-goal without a reason', 'no smallest version']);
});
test('a problem of only spaces is no problem statement', () => {
expect(specGaps({ ...good, problem: ' ' })).toEqual(['no problem statement']);
});
test('exactly 7 items is not too big', () => {
expect(specGaps({ ...good, now: ['1', '2', '3', '4', '5', '6', '7'] })).toEqual([]);
});
Show a solution
function specGaps(spec) {
const gaps = [];
const goals = spec.goals ?? [];
const nonGoals = spec.nonGoals ?? [];
const now = spec.now ?? [];
const questions = spec.openQuestions ?? [];
if (!spec.problem || !spec.problem.trim()) gaps.push('no problem statement');
if ((spec.evidence ?? []).length === 0) gaps.push('no evidence');
// == null catches undefined and null but lets 0 through
if (goals.length === 0 || goals.some((g) => g.baseline == null || g.target == null || !g.by)) {
gaps.push('goal without numbers');
}
if (nonGoals.length === 0 || nonGoals.some((n) => !n.because)) gaps.push('non-goal without a reason');
if (now.length === 0) gaps.push('no smallest version');
else if (now.length > 7) gaps.push('too big');
if (questions.some((q) => !q.owner)) gaps.push('question without an owner');
return gaps;
}Cut a wish list to the smallest useful version
Imran has collected ten ideas for the reorder app: sign in; see last orders; repeat an order with changes; order status; weekly summary; pay invoices in the app; AI-suggested items; chat with the sales rep; promotions on the home screen; save a draft order while offline. The appetite is three weeks for one developer. Sort the ten into Now, Later and Never in this version, give a one-line reason for every item outside Now, and write one goal and one guardrail with a baseline, a target and a date.
- Now covers every step a shop owner needs to repeat an order end to end, and nothing more than about five items
- Payments are a non-goal or Later, with a reason (they need finance's process, or they are a separate problem)
- Offline drafts are handled with care: the reason mentions the risk of sending an order twice or the extra work of syncing
- Every item outside Now has its own reason, not one reason for all of them
- The goal is about the pilot group, with a metric, a baseline, a target and a date; the guardrail protects delivery accuracy or orders sent twice
Show a model answer
Now: sign in, last orders, repeat with changes, order status, weekly summary. Together they let an owner repeat an order and follow it, and they fit three weeks if each is basic.
Later: AI-suggested items (we need weeks of repeat data before suggestions can be any good); chat with the rep (owners can still phone, and chat needs someone to answer it); promotions (they change what people order, which would blur what the pilot measures).
Never in this version: paying invoices in the app, because invoices stay on today's process and payments need finance's approval; offline drafts, because an order queued on the phone can be sent twice and syncing is a project of its own (the app must still survive a slow network).
Goal: pilot shops place 40% of their orders in the reorder app by the end of week 4, from 0% today, counted in the order table. Guardrail: corrections after delivery stay at 1 in 12 or better over the same four weeks.
Check yourself
A friend wants to build a booking app for her cousin's hair salon. Her spec opens with "The salon needs a modern booking app." What should she write instead?
A spec starts from a problem with evidence, so you can later tell whether the app helped and check that it is the right fix at all. Screens and a tech stack describe the solution, and "fast and easy" is a hope with nothing to measure.
Which of these is a useful non-goal for the reorder app's first version?
The first rules out something people on patchy mobile data will expect, and gives the reason, so it settles a real question and can be reopened if the reason changes. The others are goals turned negative or truisms: nobody planned crashes, bugs or unneeded features, so writing them down decides nothing.
Halfway through the three-week appetite, the builder sees the order-status screen will not be done in time. What fits the method in this lesson?
With an appetite, the time stays fixed and the scope bends, but every step of the journey stays so the slice is still useful end to end. Moving the date turns the appetite into an estimate, dropping the step removes something users need to finish the job, and overtime hides the problem instead of making a choice.
Flashcards
Key takeaways
- Start from a problem with a named user and evidence; if the statement only makes sense with your app in it, you wrote a solution.
- Goals need a metric, a baseline, a target and a date that a small pilot can move; add one guardrail.
- Non-goals with reasons stop stakeholders from waiting and stop you from building.
- Set the time first, then build a thin slice: a basic version of every step, so a user can do the whole job.
- Keep the one page in the repository with acceptance checks and open questions, and change it in the same pull request as the code.
Sources
- Shape Up: Set Boundaries (appetite and fixed time, variable scope), Ryan Singer, Basecamp, 2019
- Design Docs at Google (goals and non-goals), Malte Ubl, 2020
- The Lean Startup: principles (minimum viable product and build-measure-learn), Eric Ries