Software development has long run on an unspoken assumption: that a developer's value lives in how much code they personally write.
That assumption is now a liability.
Recently, I migrated a non-trivial production feature from a Rails app to Java Spring Boot in half a day — work that could easily have taken more than a week the old way.
That gap isn't about speed. It's about working differently.
The Old Way Was Quietly Expensive
The traditional approach to porting a feature looks like this:
- – Read the source, hold the entire mental model in your head
- – Translate it, class by class, to the new language
- – Discover edge cases mid-implementation
- – Miss the things buried four levels deep in a callback
- – Catch them in QA. Or in production
A system where cognitive load is the bottleneck. A system where the most dangerous bugs are the ones you didn't think to think about. A system where "I know this codebase" is a prerequisite that costs months to build.
Meanwhile, our tools changed. AI changed them faster than most engineering teams are willing to admit.
The Problem Was Real
Here's the short version: we had one feature that only existed in our old app, and we needed it in the new one.
Our client had been running on an older application, built in Ruby on Rails. Over time, we'd been rebuilding everything as a new application in Java, to align with where they wanted their whole stack to go. Most of the product had already made the move, but the migration is still ongoing. One feature in particular — a real, actively-used piece of functionality that talks to an outside service to verify information — was still only working in the old app. It hadn't been ported yet.
Every feature still stuck in the old app is one more thing keeping that migration from finishing. Someone had to move this one over, faithfully, without losing anything along the way.
That's a more common situation than it sounds. Any team that's ever "modernized" a system has lived through this exact moment: the new thing is 95% done, and the last 5% is one stubborn piece that nobody wants to touch because it's tangled, easy to get subtly wrong, and boring to rewrite by hand.
The scope was real:
- Three source files, each with their own edge cases
- A database layer with upsert logic and manual timestamp management
- Calls to the outside service, with version rules that differed between flows
- Image uploads that had to happen in the background, not block the user
- Error codes that mapped to specific outcomes
- Ordering rules that actually mattered to the outside service
- Two old side-integrations we'd deliberately stopped using years ago, but never removed
Doing this wrong would mean silent failures in production. Doing this right manually would mean well over a week of careful, exhausting translation work.
There was a third option.
Don't Start With Code. Start With the Spec.
The first move was not to open an IDE. It was to extract the knowledge before touching anything.
I already knew which three Ruby files held all the relevant logic. So I asked Claude to run three parallel agents — one per file — with a single directive: gather everything. Not summarize. Gather. HTTP call structures. Request body fields and their order. Response fields we needed. DB operations. Error codes. Config values. Edge cases. Log lines.
Parallel agents because speed compounds. If each file takes ten minutes, three sequential agents cost thirty minutes. Three parallel agents cost ten.
On small files, the difference is modest. On large, deeply cross-referenced codebases, it becomes the difference between a focused hour and a lost morning.
Once the agents finished, I asked for synthesis — one implementation specification document, written for building, not for reading. Concrete. Prescriptive. Every section mapping to a deliverable class or method.
That file became the contract.
Simultaneously, while those agents were working, I ran a separate agent through the Java app's existing verification-service code. Because you don't port into a vacuum. The Java app already had VerificationApiClient, VerificationConstants, a batch sync flow, established patterns. The new work needed to extend what existed — not shadow it, not duplicate it.
Two parallel tracks. Both done before a single line of Java was written.
Read What You Generated. That's the Real Work.
Here's the part people skip. I read the entire spec before asking Claude to write anything.
That's where I caught a dead third-party integration — a call to an outside service, buried in an after_commit callback in the Rails app. We hadn't used it in four years. Nobody deleted it. Claude found it, understood it, and faithfully included it in the spec. If I hadn't caught it, we'd have shipped code calling a service that no longer existed for us.
That's where I caught an abandoned experimental feature — a capability we piloted, decided against, and parked in the codebase without removing it. Also included. Also stripped before implementation started.
You couldn't have caught either one just by reading the code. The code doesn't know it's dead — it looks just as legitimate as everything still in use. The only reason it got caught is that a person remembered stopping it, and why.
The spec is where you apply judgment. The spec is where you make decisions.
That's the work AI cannot do for you. The spec is where you define what gets built and what gets left behind on purpose.
Clean the spec. Then build.
Implementation Is the Easy Part
With both documents in place — what to build, and what already existed — I asked Claude to implement the feature.
What it produced in a single pass: a controller. A router service. An authentication service. A fingerprint service. Extended API client methods sitting cleanly alongside the existing batch flow. A response DTO. An async image upload service. A typed exception class. A full set of constants.
It also wrote unit tests for every service and integration tests for the endpoint — automatically, because the conventions for doing that were already defined.
The whole thing compiled. The structure was correct. The business logic matched the spec.
One section was wrong.
Where AI Gets It Wrong — and Why That's Your Job
The Rails app triggered GCP image uploads via an after_commit callback. A native Rails pattern: fire something after the database transaction commits, outside the transaction boundary.
Claude understood the intent perfectly. It tried to replicate the pattern — and that's where it broke down.
Java has no direct after_commit equivalent the way Rails does. The code it generated around that section was convoluted, trying to approximate something that doesn't map cleanly. It wouldn't have worked.
It knows what the behavior should be. It doesn't always know the right idiom for the destination platform. That gap is yours to close.
I gave Claude context on how the Java app already handles async work, asked for a rewrite, and got back a clean @Async method — correct, idiomatic, non-blocking. The right Java answer.
- – convoluted after_commit approximation, wouldn't have worked
- + clean @Async method — correct, idiomatic, non-blocking
One correction. Thirty seconds. But only because I reviewed.
The Things That Tests Won't Catch
Beyond the one obvious issue, there's a class of bugs that are quiet. The endpoint responds. Tests pass. And then something is wrong in production because a subtle rule was violated.
These are the things I verified explicitly against the spec:
- Depending on which kind of request we're making, the outside service expects a different version number. Send the wrong one and nothing crashes — it just quietly hands back different, wrong data.
- That same service also cares about the order you ask for things in. Ask in the wrong order and the results become unreliable, even though the request itself looks perfectly fine.
- When there's nothing to report, the service doesn't send back an error — it sends back a normal "success," just with an empty result. If you only check for outright failures, you'll miss the difference between "it worked, and there's nothing here" and "it actually broke."
- Which path a record takes next isn't decided by what we asked for — it's decided by what the service actually told us back. Assume it's based on the request, and things get routed to the wrong place.
- When someone returns an item and it gets re-checked, the system has to update the original record, not create a new one — and it must never touch the fields that trace that item's history. Get this wrong and you quietly corrupt the trail of where an item's been.
- The "created" and "last updated" timestamps aren't handled automatically here — they have to be set by hand, in exactly the right places, or a record's history quietly becomes inaccurate.
Every one of these was in the spec as a checklist item. Every one would have been invisible in a standard code review.
The Wrong Question About AI
People keep asking: "Will AI replace developers?"
Wrong question. The real question is: "What kind of developer survives in an AI-native workflow?"
The answer is becoming obvious. Not the one who writes the most code. Not the one who memorizes the most syntax. Not the one who guards their knowledge because sharing it might make them redundant.
The developer who survives is the one who can:
- Understand systems deeply enough to know what questions to ask
- Provide context precise enough that AI produces accurate output
- Review output critically enough to catch what AI cannot know
- Make judgment calls about what to build and what to leave behind
- Translate behavior across paradigms when the idioms don't map
The repetitive translation work is being automated. Good. That means we can finally spend our attention on the parts that actually require us: what to build, why, and whether what was built is actually right.
The Numbers
- – Without this approach: a week or more
- + With this approach: half a day
I didn't write a single line of Java by hand. I read every line of it, made every decision about it, and caught the things it got wrong.
That's not AI replacing a developer. That's a developer who stopped doing the parts of the job that don't require a developer.
The Shift That's Already Happening
Some developers will keep doing it the old way and call it rigor. Others will hand everything to AI and call it done, then learn the hard way that automation without judgment is just faster failure.
The ones who win understand where the leverage actually is.
Not in the writing. In the thinking. Not in the volume of code produced. In the quality of decisions made.
That developer isn't being replaced. They're being accelerated.
The way we build software is changing. Not someday. Already.
The only question is whether your workflow has caught up yet.
Retour au CodeBlog