Why AI-assisted development still needs a senior developer

Why AI-assisted development still needs a senior developer

AI helped write most of a VB6-to-.NET migration – faster than a small team could – but could not replace human judgement, including right-sizing the architecture and treating legacy code as the real spec. The recurring AI mistakes were turned into rules and tooling and every AI estimate was traced and challenged before being quoted to the client.

The takeaway: AI assisted software development is great for handling the volume of work, but a senior developer still has to set the direction and verify the output – in particular on a financial system, where that verification cost doesn’t shrink over time.

AI code assistants are like F1 cars – the speed is real, but you still need a trained driver who knows the track. Otherwise you just hit the wall faster.

Mykola Hnid
Mykola Hnid
Department Manager, PMP® & .NET Technology Leader

This is how we work with AI on legacy migrations, and why a senior developer reviews the output rather than only reading its summary. 

The examples come from a recent VB6-to-.NET replatform of a skilled-nursing billing suite – 257 legacy forms, 256 stored procedures, one operator with eight facilities. AI wrote most of that system: the billing engine, the screens, the tests, the implementation plans. It did so faster and more consistently than a small team would, and that is why the project moved at the pace it did. 

What AI cannot supply is the standard to judge itself against: which design is the right size for the business, which artifact counts as the source of truth, which order of work keeps defects findable, and which scope the client is actually paying for. Those came from a person reviewing the work as it was produced. 

Key takeaways

  • Right-sized architecture, not the reference architecture. Asked for a good design, AI produces an enterprise one – multi-tenant, database-per-tenant, serverless, secret-managed. That project needed a plain ASP.NET Core API over the existing database, and got it after the first design had already been built. 
  • The legacy source is the specification. Documentation derived from VB6 loses exactly the conditions that decide a number – ByRef side effects, Currency rounding, Single versus Double promotion. Every rule is verified against the original code. 
  • Functional parity before any refactoring, including the schema. Freeze the legacy database, make the new system agree with the old one, pin that agreement with golden-master tests, and only then improve. Mixing rewrite with redesign makes a wrong figure untraceable. 
  • AI does not know the roadmap, so it must be told and the answer recorded. It optimizes the task in front of it, not the year. 
  • Recurring model errors become standing instructions and tooling. Anything corrected twice is written into the project’s rules file or automated into a checking skill. 
  • Library behavior is verified, not assumed. A model’s training cut-off makes it confidently wrong about anything that changed recently, and it does not hedge. 
  • Every figure is traced before it is quoted to a client. Including – especially – figures the AI produced. 

Going in-depth

1. Sizing the architecture to the business 

The initial design was a multi-tenant SaaS platform: Azure Functions on a consumption plan, a tenant catalogue database plus one database per tenant, per-tenant connection strings in Key Vault, identity through B2C with Static Web Apps built-in auth, and EF Core code-first against a purpose-built schema. Its own design document described it as “the minimum-viable Azure topology that’s still enterprise-credible.” For a software vendor selling to many operators, it would have been a reasonable design. 

The actual workload is forms-over-data LOB CRUD with steady internal usage: about 50 configured users, roughly 10 concurrent, growth described as heading into the hundreds as new customers are acquired but with no committed target. Functions’ real advantages – per-execution billing, event and queue triggers, elastic burst – never applied, so the app paid the isolated-worker complexity tax without using any of it. 

The replatform moved to a plain ASP.NET Core Web API with the full middleware pipeline, Dapper over the existing legacy schema, bearer-token auth, and always-on App Service hosting. Fewer moving parts, lower hosting cost, faster delivery per screen, and a far larger pool of developers who can maintain it. Roughly 10–30% of the first build was salvageable – mostly contracts, validation rules and test scenarios; every line of its data access targeted a schema that no longer applied. 

The distinction worth drawing is not “small versus large” but asked versus assumed. Present load can be measured; the trajectory can only come from the client, along with how fast new customers actually onboard and what would have to change if they did. Establishing both is what lets the larger topology be added the week a second tenant is real, rather than paid for up front. 

2. The VB6 source is the source of truth 

That project has written documentation for all 257 legacy forms and 256 stored procedures. It is genuinely useful as a navigation index, and it is not sufficient to build from. 

The clearest example: in the billing-update class, the routine that writes an invoice takes the running total ByRef – VB6’s default – and applies CInt to it before an early-exit guard that inserts nothing. So the mid-loop call that performs no work still permanently rounds the caller’s total to whole dollars. The port dropped that call as a harmless no-op, and the rounding adjustment line came out at 0.836 where the legacy wrote 1.00. 

Two more of the same family surfaced alongside it: 

  • Precision narrowing at the data-access boundary. The rate columns are SQL real. Dapper maps real to float, and assigning that into a decimal property narrows to 7 significant digits, while VB6 read the same column into a Single and promoted it to Double for the multiply. A rate of 105.7096786 became 105.7097 – visible as −0.9993 against the legacy’s −1.00 over 31 days. Fixed by widening the read with CONVERT(float, …). 
  • Rounding cadence. VB6 Currency variables round to four decimals at every assignment; decimal rounds once at the money column. The accumulation had to be re-rounded per step to match. 

Note that unit tests could not have caught either one: the in-memory fake hands clean decimals straight to the engine and never round-trips through a real column. The bug lived at the materialization boundary, which only a comparison against real output exposes. All three were found by treating a $0.0007 divergence as a defect rather than as rounding noise. 

One further caveat from that work: the legacy source itself can be stale. A refreshed copy from the client contained seven behavioral divergences hidden inside what looked like VB6 IDE re-save noise – casing and designer churn – which is why the diff was audited hunk by hunk rather than skimmed. 

3. Parity first, refactoring second 

The temptation is to fix the legacy design while rewriting it, and to modernize the schema at the same time. It reads as efficiency; it destroys diagnosis. 

When a total comes out wrong you need to know whether the fault is in the rewritten logic, in the refactoring, in data moved between the old and new database, or in a relationship between records lost or invented during that move. With four variables changing at once, every investigation searches all four. 

So the legacy schema was frozen and reused as-is, with modernization explicitly deferred to a later phase. Dapper with explicit parameterized SQL was chosen over EF Core precisely because it can be verified line by line against legacy behavior, and because a code-first model invites migrating a database you do not own. The one hardening allowed during the parity phase was non-breaking: eliminating string-concatenated SQL by routing every query through parameterized Dapper commands. 

The technical improvements then arrived deliberately late and in a deliberate order – double-post guard, observability and audit logging, golden-master tests, money-rounding helper extraction, transaction-isolation contract – with an explicit dependency that the golden-master fixtures land before the rounding refactor, so a behavior-preserving change could be proven to preserve behavior. 

The payoff shows in resolution time. Running both systems against verbatim copies of the same database and diffing the A/R output at row level meant each divergence had exactly one variable in it. A row present in the legacy output and missing from the new one was traced in a single session to an adjustments module that had not been ported, with 24 comparable rows across that month and a rule for future comparisons (filter the diff by batch type). An unexplained set of errors on a June run turned out to be the legacy writing the same errors silently and only refusing at the print step – plus a real data gap where one facility’s charge-definition table had no rows after 2023. Had the schema been redesigned and the data migrated concurrently, each of those would have been a multi-day hunt. 

4. What the AI is not told, it will assume 

  • One prepayment method had been assessed from the code as out of scope by design, not a gap – a defensible reading. One question about what the client actually runs in production turned it back into scope, and it shipped. The missing input was commercial, not technical. 
  • Billing reports moved to a separate developer and one enquiry screen was dropped by decision. Neither is inferable from the source, so both were recorded – otherwise a later session would have “closed the gap” and billed for it. 
  • Remaining scope is measured against the legacy source, not by counting finished screens. Resident Form is ~15,239 code lines with about 12% ported; one sibling form nobody had counted is 7,494 of those lines – 49% of the feature on its own. “Phase one delivered” and “12% of the feature delivered” are both true, and only the second supports a schedule. 
  • Verifying scope against production data pays for itself. A check of the financial-class configuration showed only the private class exists across every facility – no Medicare, Medicaid or insurance rows, all 1,298 charge definitions private, insurance-plan tables empty. The legacy ships full UB-92/UB-04/1500 claim classes and Medicare/Medicaid EDI machinery, and none of it is exercised by that data. That single answer removed a large body of work that could never have been validated. 

5. Corrections are paid for once 

Model errors that recurred were promoted into the project’s standing instructions or into tooling: 

What was noticedWhat it became
Screens declared complete against a write-up’s section listRule: derive the completeness checklist from the legacy form itself; first pass is grep -n ‘MsgBox’ to inventory every prompt
Tests covering the happy path onlyRule: one test per business branch, with named exemptions for unreachable race guards, verified against a coverage report 
The money-precision bugs, twice A numeric-parity section in the project gotchas: widen real reads, emulate Currency cadence, treat banker’s rounding as a per-site fact never a default 
A parity audit that passed a screen whose grouped controls had been flattened The audit skill now checks VB6 Frame group boxes — captions and  membership

Repeated work became tooling in the same way: a parity-audit skill that enumerates a legacy form’s entire surface (prompts, validations, handlers, controls, groupings) and maps each item across both stacks. Guardrails are set the same way – the agent’s database access is read-only, migrations are executed by a human, and the agent never commits or pushes. 

6. Current library behavior is verified 

A model’s most confident output describes the ecosystem as it was. On that project: Swashbuckle is broken on .NET 10, so OpenAPI moved to the built-in AddOpenApi/MapOpenApi; MUI X 8 changed the DataGrid valueFormatter signature and now renders row actions as role=”menuitem” rather than buttons, which broke tests that read as correct; one package was pinned specifically to clear a vulnerability advisory. A live documentation source was added to the workflow so library questions are answered from current docs rather than from training data. 

7. Figures are traced before they are quoted 

When the client proposed replacing React with Blazor, the first AI estimate was 165 hours. Challenging its assumptions took it to 75 across three passes: an apples-to-oranges framing that compared a rewrite against original build cost, a learning-curve budget that evaporated once it was pointed out that the AI writes Blazor the same way it wrote React, and a latency argument that collapsed because the AI had taken a city name out of a unit-test fixture and treated it as where the users work. 

The analysis also moved from Blazor Server to WebAssembly on a growth question – Server’s stateful circuits need sticky sessions and eventually a SignalR service, which does not fit a rising concurrency curve – and the final recommendation was to stay on React, with 40–80 hours quoted if the client preferred Blazor anyway and an explicit note that the backend, database and billing engine would be untouched either way. 

The same review surfaced six items that had nothing to do with the framework decision and would have been buried with it: move the cloud region nearer the users (~25-40 ms per API call), move off an undersized App Service plan, delete a dead screen calling endpoints that were never built, complete an outstanding parity audit on one billing screen, track a duplication risk between client- and server-side validation rules, and add the end-to-end tests that do not exist. 

What this means for your project 

  • AI does the volume; review sets the direction. That split is what makes the speed safe. The steering cost does fall over time, because judgment gets converted into artifacts – a rules file, a gotchas file, parity tooling, a decision register per feature. Verification cost does not fall, and on financial systems it should not. 
  • What we need from you. Access to the true source – original code, a restorable copy of production data, and the ability to run the legacy system side by side – plus decisions on scope and priority when they surface. Row-level comparison against real output is the single most effective check available, and it needs your data to work. 
  • Decisions are written, not remembered. Specs, plans and a decision register per feature, so the work survives a change of developer on either side. 

Why AI-assisted development still needs a senior developer
Mykola Hnid
11 min read

Contact us

Tell your idea, request a quote or ask us a question
We take ownership to make it a success — because we build what we’re proud of.
Yuriy Kapkovskyy linkedin
Chief Sales and Marketing Officer, Co-owner