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.

Tech Ocean
Tech Ocean
Tech Ocean
AI Coding Is Nearly Free — Why Does Delivery Still Cost So Much?

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.

A simple button connects database, message queue, third‑party services, monitoring and rollback paths
A simple button connects database, message queue, third‑party services, monitoring and rollback paths

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 log

Adding 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.

Three steps of order resubmission: block concurrency, confirm downstream result, recover interrupted tasks
Three steps of order resubmission: block concurrency, confirm downstream result, recover interrupted tasks

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?

Full delivery flow from PR review, build, compatible release to observation and incident handling
Full delivery flow from PR review, build, compatible release to observation and incident handling

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

backend engineeringobservabilityAI Codingidempotencyrollback strategySoftware Deliveryorder managementlegacy systems
Tech Ocean
Written by

Tech Ocean

Focused on AI programming, sharing ready-to-use development efficiency solutions.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.