Vibe Coding vs Spec Coding: Same Refund Feature, Built Twice A developer built the same refund feature twice—once with vibe coding and once with spec coding—to compare the outcomes. The vibe-coded version, created in 10 minutes, required two weeks of bug fixes for issues like duplicate refunds and race conditions. The spec-coded version, preceded by 90 minutes of specification writing, avoided these problems entirely. Vibe coding is intoxicating. You describe what you want in plain language, the AI writes the code, and ten minutes later you have a working endpoint. I was sold — until I shipped a refund feature that way and spent the next two weeks patching bugs that a 90-minute spec would have prevented entirely. This is the side-by-side, using the exact same requirement, so you can see where the gap opens up. An e-commerce platform needs an order refund feature. The PM's brief: Simple enough. Both paths start here. The prompt: "Build me an order refund API in Node.js. Support full and partial refunds. Call a payment gateway to reverse the charge. Track refund status. Use Express and Postgres." Sixty seconds later: a clean RefundController with createRefund and getRefundStatus . It validates the order exists, checks the amount against the order total, calls paymentGateway.refund , saves the result. The code looks professional. The happy path works. Ship it. A support agent clicks refund, the page hangs for a second, they click again. Two refunds go through. No idempotency check. Fix prompt: "Add a check to prevent duplicate refunds for the same order." The AI adds a query: if a refund exists for this order, reject. Works — until it doesn't. A $200 order. Support issues $50, then $80, then $100. Total refunded: $230. The duplicate check only catches exact duplicates, not cumulative amounts. Fix prompt: "Track cumulative refund amounts and reject refunds that would exceed the order total." The AI adds a SUM amount query — but it's not in a transaction with the insert, so two concurrent partials can both pass the check. The gateway times out. The refund row sits at processing forever. Support can't retry — the duplicate check blocks them. Did the money actually leave? Nobody knows. Fix prompt: "Add retry logic for gateway timeouts." The AI adds a retry loop: no exponential backoff, no idempotency key on the gateway call, no cap. The retry can now create a duplicate charge on the gateway side . Two agents process refunds for the same order simultaneously. Both pass the cumulative check neither refund is committed yet , both hit the gateway, both succeed. The customer is refunded twice. Fix prompt: "Add locking…" Four patches in, each reasonable in isolation, and the architecture is a patchwork: no state machine, no documented invariants, no tests for how the patches interact. The first version took 10 minutes. The four patches took two weeks — investigation, testing, support escalations, and one manual reconciliation against gateway records. The "fast" approach wasn't fast. It front-loaded the dopamine and back-loaded the pain. Same requirement. Same AI. Different starting point — 90 minutes writing this before any code: Feature: Order Refund Processing Goal Process refunds safely: no over-refund, no duplicate processing, correct gateway reconciliation. Non-Goals - Customer self-service refund portal future phase - Refund reason analytics - Automated approval rules State Machine pending → processing → succeeded pending → processing → failed → pending retry Only ONE refund may be "processing" per order at any time. Acceptance Criteria Given an order with total $200 and $0 previously refunded When a support agent requests a $50 refund Then a refund record is created with status "pending" And the gateway is called with an idempotency key And on gateway success, status moves to "succeeded" And the refundable balance is now $150. Given an order with total $200 and $150 already refunded When a support agent requests a $75 refund Then the request is rejected with "exceeds refundable balance" And no gateway call is made. Given a refund in "processing" state When another refund request arrives for the same order Then the request is rejected with "refund already in progress" And no gateway call is made. Given a refund in "processing" state When the gateway times out Then the status remains "processing" And a background job retries with exponential backoff And the retry uses the SAME idempotency key And after 3 failures, status moves to "failed" And an alert goes to the payments team. Edge Cases - Concurrency: SELECT FOR UPDATE on the order row before checking refundable balance - Idempotency: each refund attempt gets a UUID, passed to the gateway as the idempotency key - Precision: all amounts in cents integer , no floats - Reconciliation: nightly job compares local records against the gateway settlement report Rollback Plan - Feature flag: refund processing v2 - Rollback disables new refunds; in-flight ones continue via the background job - Additive schema only — no migration rollback needed Then the prompt: "Implement the refund feature described in this spec. Follow the state machine exactly. Use SELECT FOR UPDATE for concurrency control. Include the idempotency key in all gateway calls. All amounts in cents." paste spec The AI generates, in the first version : processRefund wrapped in a transaction with SELECT FOR UPDATE Every bug from Path A is pre-handled. Double refund? The lock plus the idempotency key. Overflow? Balance check in the same transaction as the insert. Timeout? Same-key retry that the gateway treats as safe. Same AI. Same capability. Dramatically different output — because the input was dramatically different. The AI didn't get smarter; it got better constraints. | Vibe coding | Spec coding | | |---|---|---| | Time to first version | 10 min | ~2.5 hours | | Production bugs | 4 one involving real money | 0 in this scenario | | Total time to stable | ~2 weeks | ~half a day | Vibe coding is great for prototypes, internal tools, and anything where a bug costs you a shrug. The moment money, state machines, or concurrency enter the picture, the 90 minutes you "save" by skipping the spec gets repaid at loan-shark interest. Adapted from the full case study on Spec Coding. The site maintains free spec templates and a browser-based spec packet generator for exactly this workflow.