PYTH-290 · Lesson 2 of 12 · 28 min
Requirements and user stories
▶ Video coming soon A video version of this lesson is on the way.
The charter says what Ridgeline Ops is for. It does not say what the software must do. Between the two sits the work of requirements: turning goals like customers can subscribe into precise statements that a developer can build and a tester can check. Vague requirements are the single most expensive problem in software, because a misunderstanding found after the code is written costs far more to fix than one found in a conversation.
In the second half of the first week, the team turned the interview notes into a backlog of user stories, wrote acceptance criteria for the most important ones, listed the quality requirements that apply to the whole system, and agreed with Rosa which stories had to be finished for demo day. This lesson follows that work and builds small tools that keep a backlog honest.
Functional and non-functional requirements
Requirements come in two broad kinds. Functional requirements describe what the system does: a subscriber can skip the next delivery; the system creates renewal orders each morning; the daily report includes subscription sales. Non-functional requirements, often called quality attributes, describe how well it does it: pages load in under two seconds on a phone; customer data stays in the country; the system can be restored from backup within four hours; staff pages are usable with a keyboard and a screen reader.
Teams often write plenty of functional requirements and forget the non-functional ones until something goes wrong. At Ridgeline the important quality attributes were easy to find once the team asked. Anita's report must be correct to the cent, so money uses integer cents and is rounded once, as the team learned the hard way in PYTH-230. Subscriber emails and addresses are personal information, so privacy law applies. And the system runs on a small budget, so whatever architecture the team chooses must cost tens of dollars a month, not hundreds.
Non-functional requirements should be measurable, just like success measures. Fast is not a requirement; the renewal job finishes in under one minute for 500 subscriptions is. Secure is not a requirement; staff accounts require multi-factor sign-in and passwords are stored only as salted hashes is.
User stories
Agile teams usually capture functional requirements as user stories. A user story is a short description of something a user wants, written from their point of view. The most common template has three parts: As a role, I want a goal, so that a benefit. For example: As a subscriber, I want to skip my next delivery, so that coffee does not arrive while I am away.
The template is not magic, but each part does useful work. The role reminds the team who the story is for, which matters when different users want different things. The goal says what they want to do, without saying how the software does it. The benefit explains why, which helps the team find simpler solutions and helps the product owner decide priority. A story with no clear benefit is often a feature someone imagined rather than a need someone has.
A user story is famously described as a placeholder for a conversation. The card itself is short; the details live in the conversation between the developers and the user, and in the acceptance criteria written down as a result. Writing a two-page specification on the card misses the point. Writing a three-word card with no conversation misses it too.
Bill Wake's INVEST checklist describes a good story. It should be Independent, so it can be built in any order; Negotiable, so the details can be discussed; Valuable to a user; Estimable, so the team can judge its size; Small enough to finish in a few days; and Testable, so everyone can agree when it is done. Stories that fail the checklist usually need splitting or rewriting.
import re
STORY = re.compile(
r"As an? (?P<role>.+?), I want (?P<goal>.+?)(?:,? so that (?P<benefit>.+?))?\.?",
re.IGNORECASE)
BACKLOG = [
"As a subscriber, I want to skip my next delivery, so that coffee does not arrive while I am away.",
"As an operations manager, I want a packing list each morning so that I can ship every subscription order on time.",
"Add a database table for subscriptions.",
"As an owner, I want a dashboard.",
]
for text in BACKLOG:
m = STORY.fullmatch(text)
if not m:
print("NOT A STORY:", text)
elif not m.group("benefit"):
print("NO BENEFIT: ", m.group("role"), "->", m.group("goal"))
else:
print("OK: ", m.group("role"), "->", m.group("goal"))OK: subscriber -> to skip my next delivery OK: operations manager -> a packing list each morning NOT A STORY: Add a database table for subscriptions. NO BENEFIT: owner -> a dashboard
The checker finds two problems in four items. Add a database table is a task, not a story: it describes work for the developers, not something a user wants. Tasks belong under the story they support. As an owner, I want a dashboard has no benefit, and it is too vague to build. When Jordan asked Rosa why she wanted a dashboard, the answer was specific: she wanted to know each Monday whether subscriptions were growing or shrinking. That became a much smaller and clearer story.
Try it yourself
Parse a user story
Write parse_story(text) that splits a story written as "As a ROLE, I want GOAL, so that BENEFIT." into a dictionary with the keys role, goal and benefit, each with surrounding spaces removed.
Accept "As a" or "As an" in any capitals, an optional comma before "so that", and an optional full stop at the end. Return None if the text does not have all three parts.
def parse_story(text):
parts = text.split(",")
return {"role": parts[0], "goal": parts[1], "benefit": parts[2]}
print(parse_story("As a subscriber, I want to skip my next delivery, so that coffee does not arrive while I am away."))Acceptance criteria
A story says what the user wants; acceptance criteria say exactly when it is done. They are the conditions that must be true for the product owner to accept the story, and they become the team's tests. Without them, a developer finishes the skip feature, the product owner tries it and says that is not what I meant, and a week is lost.
A popular format comes from behaviour-driven development: Given some starting situation, When the user does something, Then some result should follow. The format forces you to state the starting conditions, which is where most misunderstandings hide. For the skip story, the team and Rosa wrote:
- Given an active fortnightly subscription with the next delivery on 1 December, when the subscriber skips the next delivery, then the next delivery becomes 15 December and no order is created on 1 December.
- Given a subscription whose next delivery is tomorrow, when the subscriber tries to skip it, then the skip is refused because the order has already been packed.
- Given a subscription that has already been skipped twice in a row, when the subscriber tries to skip again, then the system offers to pause the subscription instead.
The second and third criteria came from asking what could go wrong. Theo pointed out that orders are packed the afternoon before, so a skip after that point would waste a packed bag. Rosa wanted repeated skipping to lead to a pause, which keeps the subscriber count honest. Neither rule was in anyone's head until the conversation forced it out, and both changed the design.
Try it yourself
Read Given, When, Then
Write parse_criteria(text) that takes acceptance criteria written one step per line and returns a list of (given, when, then) tuples, one per scenario.
Each scenario starts with a line beginning "Given", followed by a "When" line and a "Then" line. A line starting with "And" adds to the step above it, joined with " and ". Ignore blank lines and keep the text after the keyword, stripped of spaces.
def parse_criteria(text):
result = []
for line in text.splitlines():
if line.startswith("Given"):
result.append((line, "", ""))
return result
TEXT = """Given an active fortnightly subscription
When the subscriber skips the next delivery
Then the next delivery moves two weeks later
And no order is created
Given a delivery tomorrow
When the subscriber tries to skip it
Then the skip is refused"""
print(parse_criteria(TEXT))Splitting stories
The first version of the backlog had a story that read: As a subscriber, I want to manage my subscription, so that it fits my life. It is a fine summary and a terrible story, because it cannot be estimated or finished in a few days. Large stories like this are called epics, and the team split it into smaller stories, each delivering something on its own.
There are several reliable ways to split a story. Split by workflow step: sign up, change coffee, skip, pause, cancel. Split by business rule: skip with no restrictions first, then add the packing cut-off, then the repeated-skip rule. Split by data variation: fortnightly subscriptions first, then four-weekly. Split by interface: a staff page first, a subscriber page later. Or split simple from complex: one coffee per subscription first, mixed boxes later. Each slice should still cut through every layer of the system, from data to screen, so that it can be shown working. A story that delivers only the database table is a task in disguise.
After splitting, the subscription epic became nine stories and the whole backlog had twenty-three. That is too many for five weeks, which is exactly the point: now the team could see the size of the work and choose.
Prioritizing for the deadline
Rosa, as the product owner, decided priority; the developers estimated size. The team used MoSCoW, a technique from the DSDM agile method, which sorts every story into four groups. Must have stories are required for the release to be useful at all. Should have stories are important but the release still works without them. Could have stories are desirable extras. Won't have this time stories are explicitly deferred. The capital letters spell MoSCoW; the lower-case o's are only there to make it pronounceable.
The rule that makes MoSCoW work is that the Must group must fit comfortably inside the time available, usually no more than about sixty out of every hundred points of capacity. If everything is a Must, nothing is, and the team has no room to absorb surprises. Rosa found this hard at first: everything felt essential. Jordan's question helped: if this story were not finished on 18 December, would we cancel the launch? For most stories the honest answer was no.
Each story was estimated in story points, a relative measure of size and uncertainty rather than hours. The team used a short scale, 1, 2, 3, 5 and 8, and estimated by planning poker: everyone reveals a number at once, and large disagreements lead to a discussion that often uncovers a hidden requirement. Lesson 4 explains how points turn into a forecast.
CAPACITY = 60 # points the team can finish in three build sprints
BACKLOG = [
("Sign up for a subscription", "must", 5),
("Create renewal orders each morning", "must", 8),
("Packing list for operations", "must", 3),
("Skip next delivery", "must", 3),
("Daily report includes subscriptions", "must", 5),
("Stock check against the roast plan", "should", 5),
("Pause and resume", "should", 3),
("Change coffee", "should", 2),
("Weekly growth figure for the owner", "should", 3),
("Four-weekly frequency", "could", 2),
("Email reminder before each delivery", "could", 5),
("Mixed coffee boxes", "wont", 8),
]
totals = {}
for title, group, points in BACKLOG:
totals[group] = totals.get(group, 0) + points
for group in ("must", "should", "could", "wont"):
print(f"{group:6} {totals.get(group, 0):3} points")
must = totals["must"]
print(f"Must stories use {must} of {CAPACITY} points ({must * 100 // CAPACITY} in every 100)")must 24 points should 13 points could 7 points wont 8 points Must stories use 24 of 60 points (40 in every 100)
The Must group came to 24 points, well under the 60-point limit the team had agreed for Musts, leaving room for most of the Should stories and some contingency. On paper that looked almost too comfortable; in practice, as later lessons show, the renewal job alone grew from 8 points to 13 once the team understood its edge cases.
Try it yourself
Plan the minimum release
Write plan_release(backlog, capacity) where backlog is a list of (title, group, points) tuples and group is "must", "should", "could" or "wont". Return the list of titles to build, taking every Must story first, then Should, then Could, each group in backlog order, and adding a story only if it still fits in the remaining capacity. Never include "wont" stories.
If the Must stories alone do not fit, raise ValueError, because the release cannot be useful without them.
def plan_release(backlog, capacity):
return [title for title, group, points in backlog if group != "wont"]
BACKLOG = [
("Sign up", "must", 5), ("Renewals", "must", 8), ("Pause", "should", 3),
("Reminder email", "could", 5), ("Change coffee", "should", 2), ("Mixed boxes", "wont", 8),
]
print(plan_release(BACKLOG, 20))Keeping requirements alive
Requirements are not finished when the first week ends. The backlog is a living list: stories are added as people learn, split when they turn out larger than expected, and reprioritized when the business changes. What matters is that changes are visible and deliberate. At Ridgeline every new request went into the backlog with a note of who asked and why, and Rosa reviewed the order at the start of each sprint.
Two practices help. A definition of ready lists what a story needs before the team will start it: a clear benefit, acceptance criteria, an estimate and no unanswered questions. A definition of done lists what must be true before a story counts as finished: code reviewed and merged, tests passing, acceptance criteria demonstrated, documentation updated. Both definitions are short, agreed by the whole team, and written in the repository so nobody has to remember them. Lesson 4 shows Ridgeline's versions.
Common mistakes
MistakeWriting only functional requirements and discovering performance or privacy needs late.
InsteadList the quality attributes early and make each one measurable.
MistakePutting tasks such as "add a database table" in the backlog as if they were stories.
InsteadWrite stories from a user's point of view and list technical tasks under the story they support.
MistakeWriting stories without a benefit.
InsteadAsk why until the benefit is clear; it guides both priority and design.
MistakeTreating the story card as the full specification, or skipping the conversation.
InsteadUse the card as a reminder, talk with the user, and record the result as acceptance criteria.
MistakeSplitting stories by layer, such as database first and screens later.
InsteadSplit into thin slices that each work end to end, by workflow step, rule, data variation or interface.
MistakeMarking almost everything as Must have.
InsteadKeep Musts to about sixty out of every hundred points of capacity so there is room for surprises.
Summary and key terms
- Requirements turn goals into precise statements developers can build and testers can check.
- Functional requirements say what the system does; non-functional requirements say how well, and must be measurable.
- A user story names a role, a goal and a benefit, and is a placeholder for a conversation.
- INVEST describes good stories: independent, negotiable, valuable, estimable, small and testable.
- Acceptance criteria in Given, When, Then form state exactly when a story is done and become tests.
- Epics are split into thin, end-to-end stories by workflow step, rule, data, interface or complexity.
- MoSCoW sorts stories into Must, Should, Could and Won't; Musts must fit well inside capacity.
- Definitions of ready and done keep the backlog honest and are agreed by the whole team.
- functional requirement
- A statement of something the system must do.
- non-functional requirement
- A measurable statement of how well the system must perform, such as speed, privacy or availability.
- user story
- A short description of something a user wants, in the form As a role, I want a goal, so that a benefit.
- acceptance criteria
- The conditions that must be true for a story to be accepted as done.
- epic
- A story too large to finish in one sprint, which must be split into smaller stories.
- INVEST
- A checklist for good stories: independent, negotiable, valuable, estimable, small, testable.
- MoSCoW
- A prioritization method sorting work into Must, Should, Could and Won't have this time.
- story point
- A relative unit for estimating the size and uncertainty of a story.
- product owner
- The person who decides the priority of the backlog on behalf of the users and the business.
- definition of done
- The team's agreed checklist for when a story counts as finished.
Practice problems
Work each problem on paper first, then reveal the answer to check.
Rewrite "the system should be fast" as a measurable requirement for the renewal job.
Show answer
For example: the morning renewal job creates orders for 500 subscriptions in under 60 seconds.
What is wrong with the backlog item "Create the orders table"?
Show answer
It is a technical task, not a user story; it belongs under a story such as creating renewal orders.
Write a Given, When, Then criterion for pausing a subscription.
Show answer
For example: Given an active subscription, when the subscriber pauses it, then no renewal orders are created until it is resumed.
How would you split "As a subscriber, I want to manage my subscription" by workflow step?
Show answer
Into sign up, change coffee, skip, pause and cancel stories, each delivering something on its own.
The Must stories total 45 points and the team can finish 60. Is this a healthy plan?
Show answer
It is risky: 45 is 75 out of every 100 points, above the usual limit of about 60, leaving little room for surprises.
What is the difference between a definition of ready and a definition of done?
Show answer
Ready lists what a story needs before work starts; done lists what must be true before it counts as finished.
Lesson quiz · 5 questions
Score 60% or more to complete this lesson.
Create a free account to save your progress, take quizzes and earn a certificate.