AI Coding Is Nearly Free — Why Does Delivery Still Cost So Much?
The article argues that while AI tools like Claude Code accelerate initial code generation, the real engineering effort lies in defining failure states, untangling legacy systems, ensuring idempotency, rigorous testing, safe rollbacks, and post-launch observability — illustrated through an order resubmission feature that requires six non-negotiable steps before production.
With tools like Claude Code and Codex, writing the first working version of a feature — pages, APIs, unit tests — has become nearly free. But the most time‑consuming part of development was never typing code; it was everything that surrounds it. Now that coding is fast, the hidden costs are exposed.
The gap between meeting-room estimates and reality
A simple requirement — "add a resubmit button for failed orders" — sounds like half a day of work. In practice it forces six distinct engineering hurdles:
Meeting says: Add a resubmit button. Actual work: Define what "failure" means.
Meeting says: Reuse the existing submit logic. Actual work: Trace the old call chain to understand why it wasn't reused before.
Meeting says: Once the API works we can test. Actual work: Walk through concurrency, timeouts, and stuck states.
Meeting says: Merge and deploy. Actual work: DDL first, compatible release, rollback plan.
Meeting says: Deployed means done. Actual work: Watch metrics for days, wait for compensation jobs and callbacks.
1. Define "failure" before writing code
The requirement is one line: "Let users retry a failed order." The first question is not about the API but about semantics: does "failure" mean our service explicitly returned an error, or did a downstream call time out without a response? If the downstream already accepted the request, a naive retry creates a duplicate order. Testers immediately add: what about double‑clicks? Two users clicking simultaneously? Was inventory rolled back? Operations asks: can we kill the entry point if duplicates appear? None of these are in the spec; they live in old code or in manual operational workarounds. If "failure" isn't defined precisely, the faster you write code the faster you'll have to rewrite it.
2. One line of requirement, a whole legacy chain underneath
In a multi‑year Spring Boot codebase, "submit order" is rarely a single service method. It typically looks like:
Controller
-> State validation
-> Local transaction
-> Inventory handling
-> Third‑party API
-> MQ message
-> Scheduled compensation
-> Audit logAdding a new entry point means re‑validating this entire chain. The lazy approach — copy‑pasting the old method and renaming it — works for a demo but creates maintenance debt: bug fixes in the original path don't propagate, new fields added later are missing from the copy. The author's rule: keep business flow in one place . Both entry points may validate state independently, but inventory, payment, and messaging must share a single implementation.
A critical trap: @Transactional rolls back the database but cannot undo an already‑sent HTTP request. If the third party succeeds while the local transaction rolls back, the order is marked failed yet the downstream has processed it. A retry then sends a second request. An API returning failure does not mean nothing happened. Therefore, call third‑party services after the local transaction commits.
3. After HTTP 200: concurrency, timeouts, and stuck states
The first API version is trivial: POST /orders/{id}/resubmit It fetches the order, checks status, invokes the original flow, returns success. The happy path works.
To block concurrent processing, use a conditional update:
UPDATE order_task
SET status = 'SUBMITTING'
WHERE id = ? AND status = 'FAILED';One row updated → this request owns the processing; zero rows → another request won. Greying out the button in the UI is only UX, not a safety net.
Concurrency control is not idempotency. If the service crashes after setting SUBMITTING, the task stays stuck. A compensation job must scan timed‑out SUBMITTING records, query the downstream for the actual result, mark success if confirmed, or revert to FAILED if not.
Another practical issue: which order number to send on retry? Payment gateways usually deduplicate by merchant order number. Re‑using the original number either gets rejected or returns the previous result. The common pattern: generate a sub‑order number (original + sequence suffix) for the retry while keeping the original for traceability. This decision must be made before coding; retrofitting is hard.
Integration testing reveals more: backend adds a new status, old frontend doesn't recognize it; backend returns "processing", frontend shows "failed". Mini‑program and App have review cycles, so old versions may run for weeks — new status handling must be agreed upfront.
4. Don't only test the happy path
Clicking one failed order and seeing it succeed is not validation. Must also test orders in PROCESSING, COMPLETED, CANCELLED states; rapid double‑clicks; two users operating simultaneously; unauthorized direct API calls.
Historical data adds risk: new nullable columns causing errors? MQ delays causing compensation jobs to flip a already‑successful order back to failed? A backlog of old failed orders sits in the database — should the button be enabled for them? Product must decide; otherwise users retry years‑old failures and overwhelm downstream.
Test environments are clean and sequential; production has nulls, latency, restarts, and concurrency. You don't need to anticipate every surprise, but the few paths most likely to break must be exercised before launch.
5. PR merge is only code in the repo
Code review that checks only naming and formatting is useless. For this kind of change the reviewer should ask: Was the original chain reused? Is state change protected against races? What data remains if remote succeeds but local fails? Can logs correlate an entire flow via order ID and traceId? These answers aren't in the diff; they belong in the PR description.
Post‑merge deployment prep: new columns must allow nulls or have defaults; DDL before code; large‑table indexes during low traffic, not alongside release.
Rollback is not "redeploy previous version". New version may have written new statuses, emitted MQ messages, and triggered third‑party calls. Rolling back code leaves those side effects. Old code encountering unknown enum values throws exceptions, breaking the order list page. Such a rollback equals a second incident. Before launch you must decide: kill the entry point first or stop consumers? How to locate and clean dirty data?
6. Launch is not the finish line
After release, monitor resubmit volume and failure distribution; watch queue backlogs. On anomalies, pull a few orders and correlate UI, API logs, MQ messages, and downstream records.
Some issues surface later: compensation jobs run hours later; third‑party callbacks arrive late. Day one looks clean, day two an old task flips a status back. "Code deployed" only means the publish action completed. The feature is truly delivered only when it runs stably on real data, anomalies are visible, and the team can respond.
Conclusion
Writing interfaces, pages, and tests is indeed much faster now. But the saved time shouldn't be erased from the schedule. Requirement boundaries, legacy constraints, exception handling, integration, rollback — none of them disappeared. Previously estimates were padded because coding itself was slow; AI removed that padding and exposed the flawed estimation method. The first version of code is cheap. Getting it into production, making the team trust it, and being able to handle incidents — that part hasn't gotten any cheaper.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Tech Ocean
Focused on AI programming, sharing ready-to-use development efficiency solutions.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
