Case Study 02: User Stories to Working Software

Converting a Product Backlog into a Working Application Through Structured Prompting


Background

Marcus Chen is a solo developer building "MealPlan," a web application that helps busy families plan weekly meals, generate shopping lists, and track dietary preferences. Marcus has product management experience but considers himself an intermediate Python developer. He decided to use AI-assisted development to build the application, applying the user story-driven approach from Chapter 10.

Marcus had tried building a similar app six months earlier using conversational AI prompting. That attempt stalled after three weeks: the generated code was inconsistent across components, the data models kept changing as he added features, and he spent more time fixing AI output than he would have spent coding manually. This time, he committed to writing user stories with detailed acceptance criteria before generating any code.

The Product Backlog

Marcus spent one full day writing his product backlog. He organized it into three releases using MoSCoW prioritization:

Release 1 (MVP) -- Must Have: - User registration and authentication - Recipe management (create, edit, view recipes) - Weekly meal plan creation - Automatic shopping list generation

Release 1 -- Should Have: - Dietary preference profiles (vegetarian, gluten-free, etc.) - Recipe scaling (adjust servings) - Shopping list grouping by store section

Release 2 -- Could Have: - Recipe import from URLs - Meal plan sharing between family members - Nutritional information tracking

For each feature, Marcus wrote detailed user stories with acceptance criteria. Here is a representative sample:

Story 1: Recipe Creation

User Story: Create a Recipe

As a home cook,
I want to create and save my own recipes with ingredients and steps,
So that I can build a personal recipe collection for meal planning.

Acceptance Criteria:

AC-1: Basic recipe information
GIVEN I am logged in and on the "New Recipe" page
WHEN I fill in the recipe form
THEN I must provide:
  - Title (required, 3-150 characters)
  - Description (optional, max 500 characters)
  - Servings (required, integer, 1-50)
  - Prep time in minutes (required, integer, 1-1440)
  - Cook time in minutes (required, integer, 0-1440)
  - Difficulty (required, one of: easy, medium, hard)
AND I can optionally provide:
  - Photo URL
  - Source URL (if adapted from another recipe)
  - Tags (up to 10, from predefined list + custom)

AC-2: Ingredients entry
GIVEN I am creating a recipe
WHEN I add ingredients
THEN each ingredient must have:
  - Name (required, from ingredient database or free text)
  - Quantity (required, decimal, > 0)
  - Unit (required, one of: cups, tbsp, tsp, oz, lb, g, kg,
    ml, L, whole, pinch, to_taste)
  - Preparation note (optional, e.g., "diced", "minced")
AND I can add at least 1 and up to 50 ingredients
AND I can reorder ingredients by dragging

AC-3: Steps entry
GIVEN I am creating a recipe
WHEN I add cooking steps
THEN each step must have:
  - Step number (auto-assigned, sequential)
  - Instruction text (required, 10-1000 characters)
  - Duration in minutes (optional)
AND I can add at least 1 and up to 30 steps
AND I can reorder steps (numbers auto-update)

AC-4: Saving a recipe
GIVEN I have filled in all required fields
WHEN I click "Save Recipe"
THEN the recipe is saved to my collection
AND I am redirected to the recipe view page
AND I see a success message: "Recipe saved successfully"
AND the recipe appears in my recipe list

AC-5: Validation errors
GIVEN I have left required fields empty or entered invalid data
WHEN I click "Save Recipe"
THEN the form is NOT submitted
AND I see specific error messages next to each invalid field
AND the form retains all entered data
AND the page scrolls to the first error

AC-6: Draft saving
GIVEN I am partway through creating a recipe
WHEN I navigate away from the page
THEN I see a confirmation dialog: "You have unsaved changes.
     Save as draft?"
AND if I choose "Save Draft", the partial recipe is saved
    with status "draft"
AND if I choose "Discard", the data is lost
AND drafts appear in my recipe list with a "Draft" badge

Story 2: Meal Plan Creation

User Story: Create Weekly Meal Plan

As a family meal planner,
I want to assign recipes to days of the week for different meals,
So that I can organize our family's eating schedule.

Acceptance Criteria:

AC-1: Meal plan structure
GIVEN I am creating a new meal plan
WHEN the plan loads
THEN I see a 7-day grid (Monday through Sunday)
AND each day has slots for: breakfast, lunch, dinner, snack
AND each slot can hold one recipe or a text-only entry

AC-2: Adding recipes to slots
GIVEN I am viewing an empty meal slot
WHEN I click on it
THEN I see a recipe picker with:
  - Search by recipe name (instant filter, min 2 chars)
  - Filter by tags and dietary preferences
  - Filter by difficulty and prep time
  - "Quick add" text-only option (for simple meals
    like "Leftovers")
AND when I select a recipe, it fills the slot
AND I can see the recipe name and a thumbnail

AC-3: Serving adjustments per slot
GIVEN I have added a recipe to a meal slot
WHEN I click on the serving count
THEN I can adjust the number of servings for that
     specific meal (independent of the recipe's default)
AND the shopping list will use the adjusted serving count

AC-4: Meal plan dates
GIVEN I am creating a meal plan
WHEN I set the start date
THEN the plan covers that week (Monday to Sunday)
AND I cannot create two plans for the same week
AND I can create plans up to 4 weeks in advance

AC-5: Copying meals
GIVEN I have a filled meal slot
WHEN I drag it to another slot
THEN the recipe is copied (not moved) to the new slot
AND I can also copy an entire day to another day

AC-6: Shopping list generation
GIVEN I have a meal plan with at least one recipe
WHEN I click "Generate Shopping List"
THEN the system combines all ingredients from all recipes
AND quantities of the same ingredient are summed
     (unit-aware: 2 cups + 0.5 cups = 2.5 cups)
AND the list is grouped by store section
     (produce, dairy, meat, pantry, frozen, other)
AND I can check off items as I shop
AND I can add custom items to the list

Story 3: Shopping List Intelligence

User Story: Smart Shopping List

As a grocery shopper,
I want the shopping list to intelligently combine ingredients,
So that I buy the right quantities without duplicates.

Acceptance Criteria:

AC-1: Ingredient combining
GIVEN my meal plan has multiple recipes using the same ingredient
WHEN the shopping list is generated
THEN identical ingredients are combined into a single line
AND quantities are summed with unit awareness:
  - Same units: simply add (2 cups + 1 cup = 3 cups)
  - Convertible units: convert and add
    (1 cup + 8 tbsp = 1.5 cups)
  - Incompatible units: show as separate lines
    (1 whole onion + 2 cups diced onion)

AC-2: Store section grouping
GIVEN a shopping list has been generated
WHEN I view the list
THEN items are grouped into sections:
  Produce, Dairy & Eggs, Meat & Seafood, Bakery,
  Pantry & Dry Goods, Frozen, Beverages, Other

AC-3: Pantry awareness
GIVEN I have marked items as "always in pantry" (salt, pepper,
      oil, butter, etc.)
WHEN the shopping list is generated
THEN pantry items appear in a separate "Check Your Pantry" section
AND they are unchecked by default (reminder to verify)

AC-4: Quantity rounding
GIVEN ingredients have been combined
WHEN the final quantities are displayed
THEN fractional quantities are rounded to practical amounts:
  - 0.33 cups -> 1/3 cup
  - 2.5 lbs -> 2.5 lbs (no change needed)
  - 0.75 cups -> 3/4 cup
  - 3.1 whole onions -> 4 onions (round up for whole items)

The Implementation Process

With his stories written, Marcus used a systematic prompting strategy:

Step 1: Data Model Generation

Marcus combined the data implications from all stories into a single data model prompt:

Based on these user stories, design and implement SQLAlchemy 2.x
models for a meal planning application. The models must support
all the data requirements implied by the acceptance criteria.

[Pasted all three stories plus a summary of the remaining stories]

Technical requirements:
- Python 3.12, SQLAlchemy 2.x, PostgreSQL
- Use UUID primary keys
- Include created_at/updated_at timestamps
- Include appropriate indexes for the query patterns implied
  by the search and filter criteria

The AI generated models for User, Recipe, Ingredient, RecipeIngredient, RecipeStep, Tag, MealPlan, MealSlot, ShoppingList, and ShoppingListItem. Because the user stories specified exact data constraints (3-150 characters for recipe title, 1-50 servings, specific unit enums), the models included these constraints.

Marcus reviewed the models against his stories and found two issues: the AI had not included a status field on Recipe for the draft feature (AC-6 of Story 1), and it had not created the pantry items concept (AC-3 of Story 3). He added these to the models and moved on.

Step 2: Feature-by-Feature Implementation

Marcus then implemented one story at a time, always including: 1. The specific user story with all acceptance criteria 2. The relevant data models (already generated) 3. Any previously implemented code that the new story depended on

For Story 1 (Recipe Creation), his prompt was:

Implement the following user story using FastAPI with the
SQLAlchemy models provided. Create the API endpoints and any
service layer logic needed. Handle all acceptance criteria,
including validation errors (AC-5) and draft saving (AC-6).

[Story 1 full text]

[SQLAlchemy models]

Return the implementation as:
1. Router file (recipe_router.py)
2. Service file (recipe_service.py)
3. Pydantic schemas for request/response (recipe_schemas.py)

The AI generated three clean files that handled all six acceptance criteria. The validation error handling was particularly good because AC-5 spelled out exactly what should happen (specific error messages, form data retention, scroll to first error).

Step 3: The Shopping List Challenge

The shopping list intelligence (Story 3) was the most complex feature. The unit-aware ingredient combining required careful logic. Marcus's acceptance criteria proved invaluable here -- the specific examples in AC-1 (2 cups + 1 cup = 3 cups; 1 cup + 8 tbsp = 1.5 cups) gave the AI concrete test cases to work from.

The AI generated a UnitConverter class with a conversion table and a ShoppingListGenerator that: 1. Collected all ingredients from the meal plan's recipes 2. Grouped them by normalized ingredient name 3. Attempted unit conversion for each group 4. Applied the rounding rules from AC-4

Without the specific examples in the acceptance criteria, the AI would likely have implemented simple string-matching for ingredient names without the unit conversion logic. The examples made the expected behavior unambiguous.

Step 4: Integration and Testing

Marcus used the acceptance criteria to write integration tests. The GIVEN/WHEN/THEN format translated almost directly to pytest fixtures and assertions:

class TestRecipeCreation:
    """Tests derived from Story 1 acceptance criteria."""

    def test_ac1_required_fields(self, client, auth_headers):
        """AC-1: All required fields must be provided."""
        response = client.post("/api/recipes", json={
            "title": "Spaghetti Carbonara",
            "servings": 4,
            "prep_time_minutes": 15,
            "cook_time_minutes": 20,
            "difficulty": "medium"
        }, headers=auth_headers)
        assert response.status_code == 201
        data = response.json()
        assert data["title"] == "Spaghetti Carbonara"
        assert data["servings"] == 4

    def test_ac5_validation_errors(self, client, auth_headers):
        """AC-5: Specific errors for invalid fields."""
        response = client.post("/api/recipes", json={
            "title": "AB",  # Too short (min 3)
            "servings": 0,  # Must be >= 1
            "prep_time_minutes": -5,  # Must be >= 1
            "cook_time_minutes": 20,
            "difficulty": "impossible"  # Not a valid choice
        }, headers=auth_headers)
        assert response.status_code == 422
        errors = response.json()["detail"]
        field_names = [e["loc"][-1] for e in errors]
        assert "title" in field_names
        assert "servings" in field_names
        assert "prep_time_minutes" in field_names
        assert "difficulty" in field_names

Results

Marcus completed the MVP in 2.5 weeks working part-time (roughly 40 hours total). He estimated the breakdown as:

Activity Time Spent Percentage
Writing user stories and acceptance criteria 8 hours 20%
Prompting AI and reviewing output 16 hours 40%
Testing and fixing issues 10 hours 25%
Manual coding (parts AI struggled with) 6 hours 15%

Compared to his previous failed attempt (3 weeks with no working product), the specification-driven approach was dramatically more productive. The key differences:

Consistency: Because the data models were generated from a comprehensive view of all stories, they did not need to change as features were added. In his previous attempt, the models changed with nearly every new feature.

Edge case coverage: The acceptance criteria forced Marcus to think about edge cases (draft saving, ingredient unit conversion, quantity rounding) before implementation. In his previous attempt, these edge cases were discovered during testing and required significant rework.

Testability: The GIVEN/WHEN/THEN acceptance criteria translated directly to test cases, giving Marcus a test suite that he did not have to design from scratch.

AI output quality: Marcus estimated that 75% of the AI-generated code was usable with minor modifications, compared to about 40% in his previous attempt. The detailed acceptance criteria eliminated most of the "that's not what I meant" cycles.

Key Takeaways from Marcus's Experience

  1. Writing stories is thinking time, not overhead. The 8 hours spent on user stories was not wasted time -- it was the most productive thinking Marcus did on the project. Every hour of story writing saved multiple hours of rework later.

  2. Acceptance criteria are the secret weapon. The stories themselves (As a..., I want..., So that...) provided context, but the acceptance criteria provided precision. Without AC-1's specific field constraints, the AI would have made its own choices about field lengths, types, and validation rules.

  3. Concrete examples in acceptance criteria are crucial. The shopping list story's examples of unit conversion (1 cup + 8 tbsp = 1.5 cups) gave the AI exactly the specification it needed for the complex conversion logic. Abstract descriptions like "combine quantities with unit awareness" would have been insufficient.

  4. One story per prompt works better than batching. Marcus found that implementing one user story per AI prompt produced cleaner, more focused code than trying to implement multiple stories at once. Each prompt included the relevant story, the data models, and any dependencies from previous stories.

  5. The story map prevents architectural drift. By writing all stories before implementing any of them, Marcus could see the complete picture and make consistent architectural decisions. The data models generated from the full story set were stable throughout development.

  6. Acceptance criteria double as a test plan. Each GIVEN/WHEN/THEN criterion is a test case waiting to be written. This dual purpose makes acceptance criteria one of the highest-value artifacts in the development process.

Conclusion

Marcus's experience illustrates that user story-driven development with AI is not just a theoretical approach -- it is a practical workflow that a solo developer can use to build real software efficiently. The investment in writing detailed stories with comprehensive acceptance criteria pays for itself through higher-quality AI output, fewer iteration cycles, and a natural test plan that emerges from the specification itself.

The contrast between Marcus's two attempts at the same project is instructive. The difference was not in the AI model, the framework, or Marcus's coding skills. The difference was entirely in how he communicated with the AI. Structured user stories with acceptance criteria gave the AI the precision it needed to generate usable code, while conversational prompts left too much to the AI's interpretation.

For developers who are skeptical about the overhead of writing user stories, Marcus offers a simple calculation: "I spent 8 hours writing stories and 40 hours total on the project. Without stories, I spent 60+ hours and never finished. The stories did not slow me down -- they made everything else faster."