> "The first draft is never the final product --- in vibe coding, the conversation is the development process."
In This Chapter
- Learning Objectives
- Introduction
- 11.1 The Feedback Loop: The Core of Vibe Coding
- 11.2 The Critique-Modify-Improve Cycle
- 11.3 Incremental Building Strategies
- 11.4 Steering AI When It Goes Off Course
- 11.5 The Art of the Follow-Up Prompt
- 11.6 Knowing When Code Is "Good Enough"
- 11.7 Conversation Branching and Backtracking
- 11.8 Progressive Disclosure of Requirements
- 11.9 The Rubber Duck Effect: AI as Thinking Partner
- 11.10 Building Complex Systems Through Iteration
- Putting It All Together
- Chapter Summary
Chapter 11: Iterative Refinement and Conversation Patterns
"The first draft is never the final product --- in vibe coding, the conversation is the development process."
Learning Objectives
By the end of this chapter, you will be able to:
- Remember the stages of the critique-modify-improve cycle and identify them in real coding conversations. (Bloom's: Remember)
- Understand why iterative refinement is the fundamental workflow in vibe coding, distinguishing it from traditional write-then-debug approaches. (Bloom's: Understand)
- Apply structured feedback techniques to steer AI-generated code toward desired outcomes across multiple conversation turns. (Bloom's: Apply)
- Analyze conversation transcripts to identify where refinement succeeded or failed and why. (Bloom's: Analyze)
- Evaluate when code has reached an acceptable quality threshold versus when further iteration is needed. (Bloom's: Evaluate)
- Create multi-turn conversation strategies that systematically build complex systems through progressive elaboration. (Bloom's: Create)
Introduction
If you have followed the journey through Chapters 8 through 10, you now possess a solid foundation in prompt engineering fundamentals, context management, and specification-driven prompting. You know how to craft an effective initial prompt. But here is a truth that separates experienced vibe coders from beginners: the initial prompt is rarely the end of the story.
Vibe coding is not a single-shot activity. It is a dialogue. The most powerful results emerge not from a single perfect prompt, but from a skilled conversation --- a series of exchanges where you evaluate output, provide targeted feedback, and guide the AI toward increasingly refined solutions. This chapter is about mastering that conversation.
Think of it this way: a sculptor does not strike a block of marble once and reveal a masterpiece. They chip away, examine, adjust their angle, and chip again. Each strike is informed by what the previous one revealed. Vibe coding works the same way. Your prompts are chisel strikes, and the AI's responses reveal the emerging shape of your software.
We will explore the feedback loop at the heart of this process, learn specific techniques for steering AI output, develop strategies for building complex systems incrementally, and understand when to push forward versus when to backtrack. By the end, you will approach vibe coding not as a series of isolated prompts, but as a flowing conversation that converges on exactly the software you need.
11.1 The Feedback Loop: The Core of Vibe Coding
The fundamental unit of vibe coding is not the prompt --- it is the feedback loop. A feedback loop consists of three stages:
- Prompt --- You provide instructions, context, or feedback to the AI.
- Response --- The AI generates code, explanations, or proposals.
- Evaluation --- You assess the output against your requirements and mental model.
The evaluation stage feeds back into the next prompt, creating a cycle that continues until you are satisfied with the result.
+----------+ +----------+ +------------+
| Prompt | ----> | Response | ----> | Evaluation |
+----------+ +----------+ +------------+
^ |
| |
+--------------------------------------+
Feedback Loop
Why Single-Shot Prompting Falls Short
In Chapter 8, we discussed crafting effective prompts. Even the most carefully constructed prompt, however, faces inherent limitations:
- Ambiguity in natural language: Your words may carry meanings you did not intend.
- Incomplete mental models: You may not fully know what you want until you see a first attempt.
- AI interpretation gaps: The AI's understanding of your intent is probabilistic, not deterministic.
- Evolving requirements: What you need often becomes clearer as you work.
Intuition Box
Consider how you explain a task to a human colleague. You rarely give a single, perfect instruction. Instead, you describe the task, they ask clarifying questions, you see their initial approach, and you course-correct. Vibe coding follows the same natural communication pattern --- the AI just does not ask questions on its own (unless you prompt it to), so you must drive the feedback yourself.
The Compounding Value of Iterations
Each turn through the feedback loop does more than fix a single issue. It accomplishes several things simultaneously:
- Refines the AI's understanding of your intent within the conversation context.
- Reveals hidden requirements you had not considered.
- Builds shared vocabulary between you and the AI for the problem domain.
- Narrows the solution space progressively toward your ideal outcome.
Research on AI-assisted development consistently shows that developers who engage in three to five rounds of refinement produce significantly higher-quality code than those who attempt to get everything right in a single prompt. The feedback loop is not a sign of failure --- it is the process working as intended.
Feedback Loop Anatomy: A Real Example
Let us trace a complete feedback loop. Suppose you need a function that validates email addresses.
Turn 1 --- Initial Prompt:
Write a Python function to validate email addresses.
Turn 1 --- AI Response:
import re
def validate_email(email: str) -> bool:
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return bool(re.match(pattern, email))
Turn 1 --- Your Evaluation: The function works for basic cases, but you realize you need more: you want it to return specific error messages, handle edge cases like consecutive dots, and support internationalized domain names.
Turn 2 --- Refined Prompt:
Good start, but I need several improvements:
1. Return a tuple of (bool, str) where the string is an error message (empty if valid)
2. Reject emails with consecutive dots in the local part
3. Add support for internationalized domain names (IDN)
4. Add docstring with examples
This is the feedback loop in action. Your evaluation of the first response directly shaped the second prompt. Each subsequent turn narrows the gap between what you have and what you need.
11.2 The Critique-Modify-Improve Cycle
The critique-modify-improve (CMI) cycle is a structured approach to the feedback loop that gives you a repeatable framework for refinement.
The Three Phases
Phase 1: Critique --- Identify what is wrong, missing, or suboptimal in the current output. Be specific. "This is not right" is far less useful than "The error handling catches too broad an exception type on line 15."
Phase 2: Modify --- Request targeted changes. Frame your modifications clearly, distinguishing between things to add, remove, and change.
Phase 3: Improve --- After the AI applies your modifications, look for opportunities to elevate quality beyond mere correctness. This is where you push for better naming, cleaner architecture, improved performance, or more elegant solutions.
Critique Techniques
Not all critiques are equally effective. Here are proven patterns:
The Specific Pointer:
Line 23 catches a generic Exception. This should catch ValueError and TypeError
specifically, and log unexpected exceptions before re-raising them.
The Behavioral Critique:
When I pass an empty list, this function returns None instead of an empty
dictionary. It should return {} for empty input.
The Comparative Critique:
This implementation uses nested loops giving O(n^2) complexity. Can you
refactor to use a hash map approach for O(n) average complexity?
The Standards-Based Critique:
This code doesn't follow PEP 8: function names should be snake_case,
and there should be two blank lines before top-level function definitions.
Best Practice
When critiquing AI output, always specify what is wrong and why it matters. "The variable name
dis unclear" is better than "bad naming," but "The variable namedshould bedocument_cachebecause this dictionary caches parsed documents and other developers need to understand its purpose at a glance" is best.
The CMI Cycle in Practice
Here is a full CMI cycle for building a rate limiter:
Initial Prompt:
Write a Python class that implements a rate limiter using the token bucket algorithm.
AI Output (abbreviated):
class RateLimiter:
def __init__(self, rate, capacity):
self.rate = rate
self.capacity = capacity
self.tokens = capacity
self.last_time = time.time()
def allow(self):
now = time.time()
self.tokens = min(self.capacity,
self.tokens + (now - self.last_time) * self.rate)
self.last_time = now
if self.tokens >= 1:
self.tokens -= 1
return True
return False
Critique Phase:
Three issues with this implementation:
1. It is not thread-safe. Multiple threads calling allow() simultaneously
could corrupt the token count.
2. There is no way to check remaining tokens without consuming one.
3. The class lacks type hints and docstrings.
Modify Phase (AI applies changes):
The AI adds threading locks, a peek() method, and documentation.
Improve Phase:
The thread safety fix looks correct. Now let's improve further:
1. Add a context manager interface so users can do `with limiter.acquire():`
2. Add an async version for use with asyncio
3. Include a __repr__ for debugging
This three-phase approach ensures you address problems systematically before pushing for excellence.
When to Combine Phases
For simple changes, you can combine all three phases into a single prompt:
The rate limiter works but needs three changes: fix the thread safety issue
by adding a lock (critique), add a peek() method (modify), and implement
__repr__ for better debugging output (improve).
For complex changes, keeping the phases separate gives the AI more focused instructions and reduces the chance of it dropping one of your requests.
11.3 Incremental Building Strategies
One of the most effective patterns in vibe coding is incremental building --- starting with a simple, working version and progressively adding complexity. This approach mirrors how experienced software engineers build systems, but it is especially powerful with AI because each increment provides a working foundation the AI can build upon.
The Layered Approach
Think of your software as a series of layers:
Layer 4: Performance optimization, edge cases, polish
Layer 3: Error handling, validation, logging
Layer 2: Core business logic, main features
Layer 1: Basic structure, data models, skeleton
Each layer builds on the one below. You prompt the AI to build one layer at a time, validating each before moving to the next.
Layer 1 Prompt:
Create a Python class for a task manager. Start with just the data model:
- Task class with title, description, status, created_at, due_date
- TaskManager class with add_task and list_tasks methods
- Keep it simple, no validation yet
Layer 2 Prompt (after validating Layer 1):
Good, the basic structure works. Now add the core business logic:
- Filter tasks by status (todo, in_progress, done)
- Sort tasks by due date or creation date
- Mark tasks as complete with a completed_at timestamp
- Add task priority levels (low, medium, high, urgent)
Layer 3 Prompt:
Now add robustness:
- Validate that due_date is not in the past when creating tasks
- Raise custom exceptions (TaskNotFoundError, InvalidStatusError)
- Add logging for all state changes
- Validate title is non-empty and under 200 characters
Layer 4 Prompt:
Final polish:
- Add __repr__ and __str__ to all classes
- Implement task search by keyword across title and description
- Add bulk operations (mark multiple tasks complete)
- Optimize list operations for large task counts using appropriate data structures
Real-World Application
A development team at a mid-size company reported that switching to incremental building with AI reduced their defect rate by 40%. The key insight was that validating each layer caught misunderstandings early, before they could compound into deeper architectural problems.
The Scaffold-and-Fill Pattern
A variation of incremental building is the scaffold-and-fill pattern, useful for larger codebases:
- Scaffold: Have the AI generate the overall structure with stub implementations.
- Fill: Replace each stub with real implementation, one at a time.
Step 1: Create the module structure for a REST API:
- models.py with SQLAlchemy models (stubs)
- routes.py with Flask route decorators (stubs returning placeholder JSON)
- services.py with business logic function signatures (stubs with pass)
- validators.py with validation function signatures (stubs)
Step 2: Now implement the User model in models.py with all fields
and relationships.
Step 3: Now implement the user registration endpoint in routes.py
and the create_user function in services.py.
This pattern is especially effective because the scaffold gives the AI context about the entire system while allowing you to focus refinement on one component at a time.
Avoiding the "Big Bang" Anti-Pattern
The opposite of incremental building is the "big bang" --- asking the AI to generate an entire complex system in one prompt.
Common Pitfall
Resist the temptation to write one massive prompt describing your entire application. While the AI may produce something that looks complete, big-bang prompts suffer from several problems: - Errors in early parts propagate through the entire output - You cannot validate intermediate decisions - The AI's context window fills with its own output, leaving less room for quality - Debugging a 500-line output is far harder than debugging a 50-line output
If you ever find yourself writing a prompt longer than your expected code output, stop and break it into incremental steps.
11.4 Steering AI When It Goes Off Course
Even with excellent prompts, the AI will sometimes head in a direction you did not intend. Effective steering is a critical skill for vibe coders. The key is recognizing when the AI has diverged and knowing how to redirect it without starting over.
Recognizing Divergence
Common signs that the AI has gone off course:
- Wrong technology choice: You asked for a solution using
asyncioand got one usingthreading. - Over-engineering: You asked for a simple utility function and received a full class hierarchy with abstract base classes.
- Under-engineering: You asked for a production-ready solution and got a quick hack.
- Misunderstood requirements: The function signature or behavior does not match what you described.
- Style mismatch: The code follows a different paradigm or style than your existing codebase.
Steering Techniques
The Explicit Redirect: When the AI has gone clearly off track, state directly what went wrong and what you want instead.
You used threading here, but I specifically need an asyncio-based solution.
Please rewrite using async/await with aiohttp instead of requests.
The Constraint Tightening: When the AI's solution is in the right direction but too broad or too narrow, add constraints.
This works but it's over-engineered for my use case. Constraints:
- No abstract base classes; just concrete classes
- No more than 3 classes total
- No external dependencies beyond the standard library
- Aim for under 100 lines total
The Example-Driven Redirect: When words are not working, show the AI what you mean with an example.
The API you designed doesn't match what I need. Here's how I want to use it:
cache = SmartCache(max_size=100, ttl=300)
cache.set("user:123", user_data)
result = cache.get("user:123") # Returns user_data or None
stats = cache.stats() # Returns {"hits": 10, "misses": 2, "size": 45}
Please redesign the class to match this interface exactly.
The Partial Accept: When some parts are good and others are not, explicitly accept the good and redirect the bad.
The data model and the query methods are exactly what I need. Keep those.
But the caching layer is wrong:
- Replace the LRU cache with a TTL-based cache
- Remove the prefetch logic entirely
- The invalidation should be key-based, not pattern-based
Advanced
When steering the AI, pay attention to the strength of your language. "Maybe consider using asyncio" is a weak steer that the AI may ignore in favor of its current approach. "You must use asyncio. Do not use threading" is a strong steer that leaves no ambiguity. Match the strength of your language to the importance of the correction. Reserve strong language for fundamental direction changes; use softer language for style preferences.
The "Start Fresh" Decision
Sometimes steering is not enough and you need to start over. This is a judgment call, but here are signals that indicate starting fresh is more efficient than continuing to steer:
- The AI's approach has a fundamental architectural flaw that permeates the entire solution.
- You have spent more than three turns trying to redirect without progress.
- The accumulated context from failed attempts is confusing the AI.
- Your own understanding of the requirements has changed significantly.
When starting fresh, do not simply repeat your original prompt. Incorporate everything you learned from the failed attempt:
Let me restart this. I previously tried having you build this as a class
hierarchy, but that approach was too complex. Here's what I actually need:
[clearer, more specific requirements informed by the failed attempt]
11.5 The Art of the Follow-Up Prompt
Follow-up prompts are where experienced vibe coders separate themselves from beginners. A well-crafted follow-up prompt leverages the conversation context efficiently, making precise requests that the AI can execute accurately.
Follow-Up Prompt Patterns
Pattern 1: The Targeted Fix Address a single, specific issue.
In the validate_input function, the regex pattern doesn't handle Unicode
characters. Update the pattern to accept Unicode letters and add a test
case with Japanese characters.
Pattern 2: The Feature Addition Add new capability to existing code.
Add a retry mechanism to the API client. It should:
- Retry up to 3 times on 5xx errors
- Use exponential backoff starting at 1 second
- Not retry on 4xx errors (except 429)
- Log each retry attempt
Keep all existing functionality unchanged.
Pattern 3: The Refactor Request Restructure without changing behavior.
Refactor the process_order function. Currently it's 80 lines doing
validation, calculation, and persistence. Split it into three functions:
- validate_order(order) -> ValidationResult
- calculate_totals(order) -> OrderTotals
- persist_order(order, totals) -> OrderRecord
The external behavior should remain identical.
Pattern 4: The "What If" Exploration Explore alternatives without committing.
What if we used a different approach? Instead of polling the API every
30 seconds, what would a webhook-based implementation look like?
Show me the handler and the registration code, but don't change the
existing polling code yet.
Pattern 5: The Quality Gate Ask the AI to evaluate its own output.
Review the code you just generated. Are there any:
- Potential race conditions?
- Unhandled edge cases?
- Performance concerns for large inputs (100k+ items)?
- Security vulnerabilities?
List any issues you find and fix them.
Best Practice
The most effective follow-up prompts share four characteristics: 1. Specific: They identify exactly what to change. 2. Bounded: They limit the scope of changes so the AI does not inadvertently modify working code. 3. Contextual: They reference the existing code by name (function names, variable names, line numbers). 4. Purposeful: They explain why the change is needed, not just what to change.
The Follow-Up Prompt Anti-Patterns
The Vague Nudge:
Can you make it better?
This gives the AI no direction. "Better" along which dimension? Performance? Readability? Correctness?
The Kitchen Sink:
Add error handling, logging, type hints, docstrings, unit tests,
performance optimization, retry logic, caching, and make it async.
Too many changes at once. The AI will likely do several of them poorly rather than all of them well.
The Contradictory Follow-Up:
Make it simpler but also add support for all these edge cases.
Simplicity and comprehensive edge-case handling are often in tension. Acknowledge the tradeoff and state your priority.
11.6 Knowing When Code Is "Good Enough"
One of the hardest skills in vibe coding is knowing when to stop iterating. Perfectionism is the enemy of productivity, but so is shipping shoddy code. You need a framework for deciding when code meets your quality threshold.
The Quality Dimensions
Evaluate code along these dimensions, and decide which matter most for your context:
| Dimension | Question | When It Matters Most |
|---|---|---|
| Correctness | Does it produce the right output? | Always |
| Robustness | Does it handle edge cases and errors? | Production code |
| Readability | Can other developers understand it? | Team projects |
| Performance | Does it meet speed/memory requirements? | Scale-sensitive code |
| Security | Does it resist malicious input? | User-facing code |
| Maintainability | Can it be modified easily? | Long-lived code |
| Test coverage | Is it adequately tested? | Production code |
| Style | Does it follow project conventions? | Team projects |
The "Good Enough" Checklist
Before declaring an iteration complete, run through this checklist:
- Correctness verified: You have tested the primary use cases mentally or actually.
- Requirements met: All stated requirements from your original specification are addressed.
- No known bugs: You are not aware of any incorrect behavior.
- Readable code: Someone (including future you) can understand the code without the conversation context.
- Appropriate error handling: The code does not silently swallow errors or crash on reasonable inputs.
- No obvious security holes: No SQL injection, no unsanitized user input in dangerous operations, no hardcoded secrets.
If all six items pass, the code is likely "good enough" for most contexts. Additional iteration should be driven by specific concerns, not a vague sense that it could be "better."
Common Pitfall
Beware the infinite refinement trap. Each iteration has diminishing returns. The jump from "broken" to "working" is enormous. The jump from "working" to "well-structured" is significant. The jump from "well-structured" to "perfectly elegant" is often negligible in practical terms. Know which jump you are on and whether it is worth the time.
Context-Dependent Quality Standards
The appropriate quality level depends on what you are building:
Prototype / Exploration (1-2 iterations typically): - Does it demonstrate the concept? - Can you learn from it? - Good enough to show a stakeholder?
Internal Tool (2-4 iterations typically): - Does it work correctly for expected inputs? - Is the error handling adequate for your team? - Is it documented enough that a teammate could use it?
Production Code (3-7 iterations typically): - Does it handle all edge cases? - Is it tested? - Does it follow project conventions? - Is it secure against adversarial input? - Will it perform at expected scale?
Library / Public API (5-10+ iterations typically): - Is the API intuitive and consistent? - Is the documentation comprehensive? - Is backward compatibility considered? - Are error messages helpful to end users?
11.7 Conversation Branching and Backtracking
Not every conversation follows a straight line from initial prompt to final solution. Sometimes you need to explore multiple approaches, backtrack from dead ends, or branch the conversation to compare alternatives.
When to Branch
Branching means exploring an alternative approach without abandoning your current one. This is useful when:
- You are unsure which of two architectures is better.
- You want to compare performance characteristics of different approaches.
- A stakeholder has suggested an alternative and you want to evaluate it.
Before we continue with the current approach, let's explore an alternative.
Keep the current implementation in mind, but show me what this would look
like using an event-driven architecture instead of the request-response
pattern we have now. I want to compare the two approaches.
When to Backtrack
Backtracking means abandoning the current direction and returning to a previous state. Backtrack when:
- The current approach has revealed a fundamental flaw.
- Continued iteration is yielding diminishing returns or making things worse.
- You have learned something that invalidates your earlier assumptions.
Let's go back to the version from three messages ago, before we added
the caching layer. The caching is causing more complexity than it saves.
Let's instead focus on optimizing the database queries directly.
Advanced
In tools like ChatGPT, Claude, and Cursor, you can literally branch conversations by editing a previous message. This creates a fork in the conversation tree. Use this feature strategically: - Fork after your best prompt to try different AI responses. - Fork before a major direction change so you can return to the other branch if needed. - Fork when the AI produces an interesting alternative you want to explore later.
If your tool does not support branching, you can simulate it by copying the relevant code and context into a new conversation.
The Explore-Compare-Decide Pattern
For important architectural decisions, use this structured approach:
- Explore Option A: Have the AI implement the first approach.
- Explore Option B: In the same or a parallel conversation, implement the alternative.
- Compare: Ask the AI to analyze tradeoffs between the two.
- Decide: Choose the approach that best fits your constraints.
We've now seen both approaches. Compare them:
Approach A (Event-Driven):
[paste or reference the event-driven version]
Approach B (Request-Response):
[paste or reference the request-response version]
Compare them on: complexity, testability, performance under load,
and ease of adding new features. Which would you recommend for a
system that needs to handle 10,000 concurrent users?
Managing Conversation Length
Long conversations accumulate context, which can be both a blessing and a curse. Benefits include the AI remembering earlier decisions and maintaining consistency. Drawbacks include the AI becoming confused by contradictory instructions from earlier turns and the context window filling up.
Signs your conversation is too long: - The AI starts "forgetting" requirements you stated earlier. - Responses become inconsistent with earlier decisions. - The AI references code that has since been replaced. - Output quality noticeably decreases.
Solutions: - Start a new conversation with a clear summary of where you are. - Use the technique from Chapter 9 of providing a condensed context block. - Break the work into independent modules that can each be their own conversation.
11.8 Progressive Disclosure of Requirements
Progressive disclosure is a technique where you deliberately reveal requirements to the AI in stages rather than all at once. This might seem counterintuitive --- wouldn't giving the AI all information upfront be better? Not necessarily.
Why Progressive Disclosure Works
- Reduces cognitive load on the AI: Simpler prompts produce more focused, accurate code.
- Produces better architecture: Code designed to handle one thing well is often more extensible than code designed to handle everything from the start.
- Creates natural validation points: Each stage gives you a working version to evaluate.
- Mirrors real development: Requirements rarely arrive all at once in practice.
The Progressive Disclosure Pattern
Stage 1 --- Core Functionality:
Build a user authentication system with signup and login using
email and password. Use bcrypt for password hashing. Return JWT tokens.
Stage 2 --- After validating Stage 1:
Now add password reset functionality. The user should be able to request
a reset link that expires after 1 hour. When they click the link, they
can set a new password.
Stage 3 --- After validating Stage 2:
Add OAuth2 support. Users should be able to sign in with Google or GitHub
in addition to email/password. Link OAuth accounts to existing email
accounts if the email matches.
Stage 4 --- After validating Stage 3:
Add two-factor authentication (2FA) using TOTP. Users can enable 2FA in
their settings. When enabled, login requires both password and TOTP code.
Notice how each stage builds naturally on the previous one, and the code from earlier stages does not need to be rewritten --- it just gets extended. If you had specified all four stages upfront, the AI might have produced a monolithic authentication system that was harder to understand and debug.
When NOT to Use Progressive Disclosure
Progressive disclosure is not always appropriate. Consider disclosing requirements upfront when:
- Architectural decisions depend on later requirements: If knowing about OAuth support would change how you structure the authentication system, mention it upfront even if you will implement it later.
- The AI needs to design for extensibility: A heads-up like "We will later add OAuth and 2FA" lets the AI make better early design decisions.
- Requirements are tightly coupled: If features interact heavily, the AI needs to know about all of them to design correct interfaces.
Intuition Box
Think of progressive disclosure like giving directions. If someone asks how to get to the restaurant, you do not start with "In 3 miles, turn left." You say "Head north on Main Street." Once they are on Main Street, you give the next instruction. But if there is a tricky interchange where they need to be in a specific lane now for a turn that happens in 2 miles, you warn them about it early. Apply the same logic to requirements: disclose later requirements early only when they affect current decisions.
The Hybrid Approach
The most effective strategy combines progressive disclosure with strategic foreshadowing:
Build a user authentication system with signup and login. Use bcrypt and JWT.
Note: We will later add OAuth, password reset, and 2FA, so please design
the user model and auth service with extensibility in mind. But for now,
just implement basic email/password auth.
This gives the AI enough context to make good architectural decisions without overwhelming it with implementation details it does not need yet.
11.9 The Rubber Duck Effect: AI as Thinking Partner
The "rubber duck" debugging technique --- explaining your problem aloud to an inanimate object to spark insight --- is a well-known practice in software engineering. AI takes this concept to an entirely new level because unlike a rubber duck, it talks back.
Beyond Debugging: AI as a Thinking Partner
Using AI as a thinking partner goes beyond debugging. You can use it to:
Explore design spaces:
I'm designing a notification system. I'm torn between a push-based
approach (server pushes notifications to clients) and a pull-based
approach (clients poll for notifications). What are the tradeoffs?
What would you recommend for a mobile app with 50,000 users where
timely delivery matters but battery life is a concern?
Validate your reasoning:
I'm thinking of using a microservices architecture for our new e-commerce
platform. My reasoning is that different teams can deploy independently,
and we can scale the product catalog service separately from the order
processing service. But we're a team of 5 developers. Am I over-engineering
this? What would you recommend?
Discover hidden assumptions:
Here's my current data model for the scheduling system:
[paste model]
What assumptions am I making that might be wrong? What edge cases
have I not considered?
Pressure-test solutions:
Here's my proposed caching strategy:
[paste strategy]
Play devil's advocate. What could go wrong? Where are the failure modes?
How would this behave under a thundering herd scenario?
Real-World Application
A senior developer at a fintech company described how she used AI conversations to discover a subtle race condition in her payment processing system. She was not even looking for bugs --- she was asking the AI to explain her system back to her in simple terms. When the AI's explanation diverged from her mental model, she realized the divergence pointed to an actual flaw in the code. The AI did not find the bug; the conversation did.
Structured Thinking Conversations
For complex design decisions, use a structured conversation format:
Step 1 --- State the problem:
I need to design a data pipeline that processes 10 million events per day.
Events come from 50 different sources in different formats. They need to be
normalized, enriched with data from our user database, and stored in a
data warehouse for analytics.
Step 2 --- Explore constraints:
My constraints are:
- Budget: We can spend $500/month on infrastructure
- Team: 2 backend developers, neither has Kafka experience
- Latency: Events should be queryable within 5 minutes of arrival
- Reliability: We cannot afford to lose events
What architectures fit these constraints?
Step 3 --- Evaluate options:
You suggested three approaches. Let's evaluate each against my constraints.
For each, estimate: monthly cost, learning curve, latency achieved,
and reliability guarantees.
Step 4 --- Deep-dive on the chosen approach:
Option 2 looks best. Let's flesh it out. Show me the component diagram,
the data flow, and the key implementation decisions we need to make.
This conversation pattern turns the AI into a collaborator who helps you think through the problem, not just a code generator that produces output.
The Socratic Method with AI
You can also flip the dynamic and have the AI ask you questions:
I want to build a task management application. Instead of jumping to
implementation, I'd like you to interview me about the requirements.
Ask me questions one at a time to understand what I need. Challenge my
assumptions where appropriate.
This approach surfaces requirements you might not have considered and forces you to articulate your thinking clearly --- which often reveals gaps in your own understanding.
11.10 Building Complex Systems Through Iteration
Everything we have discussed in this chapter converges when building complex systems. A complex system cannot be built in a single prompt or even a single conversation. It requires a deliberate strategy that combines incremental building, progressive disclosure, steering, and quality evaluation.
The System-Building Workflow
Phase 1: Architecture Conversation Start with a design-focused conversation. Do not write code yet.
I need to build an inventory management system for a warehouse.
Let me describe the requirements, and I'd like you to propose
an architecture.
Requirements:
- Track products across multiple warehouse locations
- Handle incoming shipments and outgoing orders
- Support barcode scanning for inventory updates
- Generate reports on stock levels and movement
- Alert when stock falls below reorder thresholds
Propose a high-level architecture. What are the main components?
How do they interact?
Phase 2: Foundation Building Build the foundational layers --- data models, core abstractions, project structure.
Let's start implementing. Begin with the data models:
- Product (SKU, name, description, category, weight, dimensions)
- Location (warehouse, zone, shelf, bin)
- InventoryItem (product + location + quantity)
- Use SQLAlchemy with type hints
Phase 3: Vertical Slice Implement one complete feature path from API to database.
Now implement a complete vertical slice: the "receive shipment" workflow.
This should include:
- API endpoint: POST /shipments/receive
- Service layer: process the shipment, update inventory
- Database operations: create shipment record, update quantities
- Basic validation
Phase 4: Horizontal Expansion With the vertical slice working, expand to other features using the same patterns.
Using the same patterns from the shipment receiving code, implement:
1. POST /orders/fulfill - Process an outgoing order
2. GET /inventory/{sku} - Get current stock across all locations
3. POST /inventory/transfer - Move stock between locations
Follow the same service layer pattern we established.
Phase 5: Cross-Cutting Concerns Add system-wide capabilities.
Now add cross-cutting concerns to all existing endpoints:
1. Authentication middleware (JWT-based)
2. Request logging with correlation IDs
3. Rate limiting (100 requests/minute per user)
4. Standardized error response format
Phase 6: Hardening Focus on quality, edge cases, and production readiness.
Let's harden the system:
1. Add input validation for all endpoints using Pydantic
2. Handle concurrent inventory updates (optimistic locking)
3. Add database migrations with Alembic
4. Create health check endpoint
5. Add request/response schema documentation
Best Practice
When building complex systems, keep a running document outside the AI conversation that tracks: - Architectural decisions made and why - Components completed and their status - Known issues and technical debt - Interfaces between components
This document serves as your source of truth and provides context you can paste into new conversations when the current one grows too long.
Managing Multiple Conversations
Complex systems often require multiple parallel conversations:
- Conversation 1: Backend API implementation
- Conversation 2: Database schema and migrations
- Conversation 3: Frontend components
- Conversation 4: Testing and quality assurance
The challenge is maintaining consistency across conversations. Strategies include:
- Shared interface definitions: Define your API contracts, data models, and interfaces in a document that you paste into each conversation.
- Integration checkpoints: Periodically have a conversation focused solely on integration: "Here is the backend API. Here is the frontend that consumes it. Are they compatible?"
- Single source of truth: Designate one conversation (or external document) as the authority on architectural decisions.
Iteration Metrics
Track your iteration patterns to improve over time:
- Iterations per feature: How many turns does it take to get a feature right? If it is consistently high, your initial prompts may need improvement (revisit Chapter 8).
- Backtrack rate: How often do you need to undo and restart? High rates suggest a need for better upfront design conversations.
- Time to "good enough": How long from first prompt to acceptable quality? Tracking this helps you estimate future work.
- Common correction types: Do you always end up asking for the same kinds of fixes? If so, include those requirements in your initial prompts or create a standard prompt template (see Chapter 10).
Common Pitfall
Do not conflate "number of iterations" with "quality of process." Some features legitimately require many iterations because they are complex. The goal is not to minimize iterations but to make each iteration productive. A conversation that takes eight focused turns and produces excellent code is better than one that takes two turns and produces mediocre code.
Putting It All Together
The techniques in this chapter form a cohesive workflow for vibe coding:
- Start with a clear but focused initial prompt (Chapters 8-10).
- Evaluate the response against your requirements and mental model.
- Apply the CMI cycle --- critique what is wrong, modify what needs changing, improve what can be elevated.
- Build incrementally --- do not try to get everything in one prompt.
- Steer when off course --- use the right technique for the degree of divergence.
- Craft targeted follow-ups --- specific, bounded, contextual, purposeful.
- Know when to stop --- use the quality dimensions and checklist.
- Branch and backtrack as needed --- not every path leads forward.
- Disclose requirements progressively --- but foreshadow when architecture depends on future features.
- Use the AI as a thinking partner --- not just a code generator.
- Scale to complex systems --- combine all techniques with deliberate multi-phase, multi-conversation strategies.
Vibe coding is a skill that improves with practice. Each conversation teaches you something about how to communicate with AI more effectively. Over time, your iterations will become fewer, your feedback more precise, and your results more consistently excellent.
Chapter Summary
This chapter explored the iterative nature of vibe coding and the conversation patterns that make it effective. The feedback loop --- prompt, response, evaluation --- is the fundamental unit of work. The critique-modify-improve cycle provides a structured framework for refinement. Incremental building strategies help you construct complex systems by layering capability on validated foundations. Steering techniques redirect the AI when it diverges from your intent, while well-crafted follow-up prompts make each iteration maximally productive.
We examined how to determine when code is "good enough," recognizing that the appropriate quality bar depends on context. Conversation branching and backtracking give you the flexibility to explore alternatives without losing progress. Progressive disclosure of requirements mirrors real-world development and often produces better-architected code than upfront specification. The rubber duck effect transforms AI from a code generator into a thinking partner. Finally, we developed strategies for building complex systems that combine all these techniques into a coherent, multi-phase workflow.
The core insight of this chapter is that vibe coding is not about writing the perfect prompt --- it is about conducting an effective conversation. The prompt is just the beginning. The real skill lies in what you do with the response.
In the next chapter, we will explore advanced prompting techniques that go beyond the fundamentals, including chain-of-thought prompting, few-shot learning, and meta-prompting strategies that can dramatically improve AI output quality.