From Vibe Coding to Spec Coding: A Practical Migration Guide for Developers
I deployed a feature that looked perfect in my AI coding session. Two weeks later, I was debugging cart persistence issues, inventory validation gaps, and checkout failures I never anticipated. The code worked—I had confirmed it visually. But I had no spec to check against when things went wrong.
That’s when I realized: Vibe Coding gave me a fast start, but it also gave me a slow finish.
The Hidden Cost of “Looks Good”
I used to love Vibe Coding. I’d describe what I wanted to an AI assistant, get back working code, confirm it visually, and move on. Speed was the metric. Lines per minute. Features shipped.
But then came the recurring patterns:
Day 1: "Add a shopping cart" → 500 lines generated → "Looks good!" → DeployDay 14: Cart doesn't persist → User complaints → Debug sessionDay 15: Found missing inventory check → Quick fix → DeployDay 16: Checkout fails on edge case → Another debug sessionDay 17: What did I actually build?The problem wasn’t the AI. The problem was me—I was building without a contract. Every session was a new interpretation. Every feature was a fresh negotiation with the AI about what “working” meant.
I needed something I could point to and say: “This is what I intended. Does the implementation match?”
My First Attempt at Spec-Driven Development
I decided to try writing a spec before coding. I opened a markdown file and started typing:
## Shopping Cart Feature
Users should be able to:- Add items to cart- Remove items from cart- View cart contents- Checkout
Technical implementation:- Use Redis for storage- Handle edge casesI fed this to my AI coding tool and got… the same ambiguous result. The spec was too vague. It didn’t constrain the solution space. It wasn’t a spec—it was a wish list.
Discovering the Spec Structure
I studied how engineering teams write specs. Not PRDs—those are too high-level. Not tickets—those are too granular. I needed functional specifications with acceptance criteria.
Here’s what I learned:
┌─────────────────────────────────────────┐│ FEATURE SPEC ││ What + Why (functional requirements) │├─────────────────────────────────────────┤│ TECHNICAL PLAN ││ How (technical approach) │├─────────────────────────────────────────┤│ TASKS ││ Implementation breakdown │└─────────────────────────────────────────┘The key insight: Functional specs describe behavior, not implementation. They’re written in terms a non-technical stakeholder could understand:
## Feature: Shopping Cart
### Use Cases- Customer adds products for later purchase- Customer adjusts quantities before checkout
### Acceptance Criteria- Given user adds item, when cart is not at capacity, then item appears in cart with quantity 1- Given user adds same item, when item already in cart, then increment quantity (max 10)- Given user removes item, when quantity > 1, then decrement quantity; otherwise remove item- Given user session expires, when user returns, then cart persists from previous session- Given item goes out of stock, when user views cart, then show "out of stock" indicator and prevent checkoutNow I had something concrete. Something I could review with teammates. Something the AI could reference for consistent behavior.
Step 1: Start with Plan Mode (No Tool Changes Required)
Before I could write specs, I needed to build the habit. I didn’t change my tools—I just changed my workflow.
In Claude Code, I started using Plan Mode before every coding session:
Before: User → "Add shopping cart" → AI generates code → User reviews
After: User → "Add shopping cart" → AI creates plan → User reviews plan → AI generates codeThe shift was subtle but powerful. I was now forced to think through the approach before seeing any code. Even a 30-second plan changed how I framed problems.
My first few plans were terrible:
Plan:1. Create cart component2. Add state management3. Connect to backend4. Style itBut the AI pushed back: “What backend? What state management approach? What should happen when inventory is low?”
That pushback was the training I needed. I was learning to think before I typed.
Step 2: Write Your First Real Spec
After a week of Plan Mode practice, I tackled a medium-complexity feature: user notification preferences. Not trivial (not “add a button”), not massive (not “rebuild auth”). Something that would take 2-4 hours with Vibe Coding.
I followed the spec template religiously:
## Feature: User Notification Preferences
Users control how and when they receive notifications.
### Use Cases- User enables email notifications for order updates- User disables push notifications during quiet hours- User configures digest frequency for marketing emails
### Acceptance Criteria- Given user visits settings, when notifications section loads, then display current preferences grouped by channel (email, push, SMS)- Given user toggles notification type, when toggle completes, then save preference immediately (no save button) and show confirmation toast- Given user sets quiet hours, when time is within quiet hours range, then suppress push notifications and queue for next active window- Given user unsubscribes from marketing, when using one-click unsubscribe link, then update preference and show confirmation page (no login required)
### Edge Cases- User has no verified email address → Show "Verify email first" message- User has no verified phone → Disable SMS toggles with explanation- Third-party notification service down → Queue changes locally, retry with exponential backoff
### Technical Constraints- Changes must reflect within 100ms (optimistic UI updates)- Preference data stored in user_preferences table- Use event sourcing for audit trail of preference changesThis spec took me 20 minutes to write. The implementation took 2 hours. But when I found a bug two days later—quiet hours weren’t persisting—I knew exactly where to look. The acceptance criteria told me what should happen. The gap was clear.
Step 3: Use Tooling to Enforce Structure
After writing a few specs manually, I discovered I was inconsistent. Sometimes I forgot edge cases. Sometimes I mixed functional requirements with technical details.
Spec Kit (from GitHub) helped standardize my approach:
$ specify init . --ai cursor-agent
# Generated structure:my-project/├── spec/│ ├── SPEC.md # What + Why (functional)│ ├── PLAN.md # How (technical)│ ├── TASKS.md # Implementation breakdown│ └── IMPLEMENTATION.md # Execution log└── .cursor/ └── rules/ # AI-specific rulesThe tool didn’t write specs for me—it created the structure that reminded me what to fill in. Each file had a purpose:
SPEC.md → Anyone can read and understandPLAN.md → Engineers understand implementation approachTASKS.md → Checkboxes for implementation progressIMPLEMENTATION.md → What actually happened (for future reference)Step 4: Embrace Imperfect Specs (They Get Better)
My first specs were embarrassingly incomplete. I missed edge cases. My acceptance criteria had gaps. I conflated functional and technical concerns.
But here’s what I discovered: Fixing a spec is dramatically easier than fixing unstructured code.
Vibe Coding Debug: 1. Read 500 lines of code 2. Understand the implementation 3. Guess what was intended 4. Find the gap 5. Fix code + hope no side effects
Spec Coding Debug: 1. Read acceptance criteria 2. Identify which criteria is violated 3. Check implementation against that criteria 4. Fix the specific gapWhen I deployed that shopping cart feature with specs, I found a bug in inventory validation. The spec said:
Given item goes out of stock, when user views cart,then show "out of stock" indicator and prevent checkoutI checked the implementation. The indicator showed, but checkout wasn’t prevented. The gap was obvious—two lines of code. Fixed in 5 minutes. Deployed confidently.
Common Mistakes I Made (So You Don’t Have To)
Mistake 1: Trying to Spec Everything at Once
I spent a full day writing specs for an entire authentication system. By day two, requirements had changed. I had wasted time on specs that were immediately obsolete.
Lesson: Spec one feature at a time. Ship it. Then spec the next.
Mistake 2: Making Specs Too Technical
## Auth SystemUse JWT with RS256 signing.Token expiry: 15 minutes.Refresh token in HTTP-only cookie.Store sessions in Redis with TTL.This isn’t a spec—it’s an implementation decision. Non-technical stakeholders can’t validate this. It belongs in PLAN.md, not SPEC.md.
Correct approach:
## AuthenticationUsers can log in and remain logged in across sessions.
### Acceptance Criteria- Given user logs in, when credentials are valid, then user is authenticated and redirected to dashboard- Given user closes browser, when user returns within 7 days, then user is still authenticated (no re-login required)- Given user logs out, when logout completes, then user is unauthenticated on all devicesTechnical decisions follow from functional requirements, not the other way around.
Mistake 3: Skipping Plan Mode for “Simple” Features
I thought I could skip Plan Mode for a simple “add a button” feature. Three hours later, I had refactored the entire component architecture because I hadn’t considered state management implications.
Lesson: Even 30 seconds of planning prevents 3 hours of refactoring.
Mistake 4: Abandoning Vibe Coding Entirely
I went cold turkey on Vibe Coding. Every feature got a full spec, plan, and task breakdown. My velocity dropped. I got frustrated. I almost gave up on Spec Coding entirely.
Lesson: Vibe Coding still has a place—for quick prototypes, throwaway experiments, and learning. The goal is adding a spec layer, not replacing everything.
Vibe Coding: Prototype, spike, throwaway demo, learningPlan Mode: Small features, bug fixes, straightforward changesFull Spec: Medium+ complexity, production features, team projectsThe Migration Path: Layer by Layer
The transition from Vibe Coding to Spec Coding isn’t a replacement—it’s an addition:
Layer 0: Vibe Coding (baseline) ↓ Add Plan ModeLayer 1: Plan Mode habit ↓ Add lightweight specsLayer 2: Spec-first for medium complexity ↓ Add toolingLayer 3: Structured spec directories ↓ Iterate and refineLayer 4: Spec-native developmentEach layer adds capability without removing what came before. I still use Vibe Coding for quick prototypes. I still use Plan Mode for small features. But for anything that matters—production features, team projects, complex logic—I start with a spec.
What Changed for Me
Before Spec Coding, I spent my time like this:
Building: ████████████████░░░░ 60%Debugging: ████████████░░░░░░░░ 40%Documenting: ░░░░░░░░░░░░░░░░░░░░ 0%After Spec Coding:
Specifying: ████████░░░░░░░░░░░░ 20%Building: ████████████░░░░░░░░ 40%Debugging: ████░░░░░░░░░░░░░░░░ 15%Documenting: ████████░░░░░░░░░░░░ 25% (specs ARE documentation)Total time is similar. But the debugging time dropped dramatically. And I now have documentation that stays current because it was written before the code.
Starting Today
You don’t need new tools. You don’t need to rewrite existing code. You just need to:
- Enable Plan Mode in your AI coding tool. Use it before every prompt.
- Write one spec for a medium-complexity feature you’re working on this week.
- Use the structure: Functional requirements (SPEC.md) → Technical approach (PLAN.md) → Tasks (TASKS.md).
- Iterate. Your first spec will be rough. Your tenth will be better. Your hundredth will be second nature.
The code you write today will be debugged by someone tomorrow. Sometimes that someone is you. A spec is a gift to your future self.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 Claude Code Documentation
- 👨💻 Spec Kit GitHub
- 👨💻 Cursor AI
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments