Enterprise AI stalls on legacy systems because the systems give AI no context. They have no API, no documentation, no access for new tools, and no logs. The fix starts below the AI: a foundation layer that makes the old system readable, then a step-by-step rewrite that proves each new part works before it takes over. OpenNash does this with a forward-deployed engineer (FDE) on your team and OpenNash Software Factory, our coding agents for legacy code.

This post covers our process, how we choose between Go and Rust, and how we prove a rewrite with parallel runs and formal verification.

Why Enterprise AI Fails on Old Systems

Gartner found that 63% of organizations do not have, or are not sure they have, the right data management practices for AI. It predicts that through 2026, organizations will abandon 60% of AI projects that lack AI-ready data (Gartner, February 2025).

The model is rarely the problem. An agent can only act on what it can read and call. Anthropic's engineering team calls context "a finite resource with diminishing marginal returns" (Anthropic, Effective context engineering for AI agents). An old system gives the agent almost none of it.

Gap What it looks like What it blocks
No API Data lives in an IBM i library, Oracle Forms, or a Visual Basic 6 app Agents cannot read or act
No access New tools need a ticket, a VPN account, and a security review Every pilot waits months
No visibility No logs, no event stream, batch jobs that run at 2 a.m. Nobody can measure what the system does
No documentation Business rules live in code and in two people's heads Agents and new engineers guess
No tests Nobody knows what a change breaks Every change is a risk, so nothing changes

The same gaps block modernization. AWS lists code base access as the first requirement of the strangler fig pattern. In its words, "You cannot intercept calls without code base access" (AWS Prescriptive Guidance, Strangler fig pattern). So the first work is the same for an AI project and for a rewrite. Build the foundation layer.

The Foundation Layer Comes First

The foundation layer is the set of interfaces that let people, agents, and new code see and use the old system safely. It does not replace anything. It makes the system visible.

Part What we build Who uses it
Read-only access A read-only API or replica over the legacy database Agents, dashboards, new services
Event capture Change data capture or log shipping for the key tables and jobs The parallel run, monitoring
Governed tools MCP servers over the systems of record, scoped to each user and audited AI assistants and agents
System map Modules, data flows, jobs, and external interfaces, written by agents and checked by engineers Everyone
Business rule register Each rule, where it lives in code, who owns it, and whether to keep or fix it SMEs, engineers, auditors
Golden datasets Real inputs and outputs, with edge cases and month-end runs Tests and the parallel run

The foundation layer pays off before any rewrite. Once an agent can read orders, inventory, and claims through a governed tool, teams can automate work on top of the old system. For how we build those tools, see the MCP server tutorial and data and context engineering for AI agents.

Most of the Work Is People

The code is the fast part. The slow part is learning what the system must do. That knowledge sits with subject matter experts (SMEs) in finance, operations, and IT, and they rarely sit in the same meeting.

An OpenNash FDE works inside your team, on site when possible. Most of the FDE's time goes to these jobs:

  1. Interview the SMEs. Ask each expert to walk through real cases, including the exceptions they fix by hand.
  2. Run the cross-functional meetings. Put finance, operations, IT, and security in one room to agree on rules, owners, and edge cases.
  3. Turn answers into artifacts. Each meeting ends with updates to the rule register, the golden datasets, or the acceptance criteria.
  4. Get access. Work with security and IT to approve read-only access, service accounts, and audit logs.
  5. Close the loop. Show each SME the differences the parallel run finds, and record their decision.
Who is in the room What they know What we ask
Process owner (for example, the controller) Why each rule exists Which outputs must never change?
Daily operators The workarounds and manual fixes What do you fix by hand each week?
Legacy engineers Where the rules live in code Which jobs and tables does this touch?
IT and security Access, networks, compliance What access can we have, and how is it audited?
Finance or audit Controls and reports What evidence do you need before cutover?
Business sponsor The budget and the goal What result makes this worth it?

Agents make this faster. They read the code and draft the system map before the first interview. The FDE then asks better questions, and the SMEs correct a draft instead of starting from zero.

The OpenNash Modernization Process

We do not replace the whole system at once. We use the strangler fig pattern. A routing layer sends one function at a time to the new code. The old system keeps running everything else (Martin Fowler, Strangler Fig).

  1. Embed. An FDE joins your team. In week one we agree on one bounded target with a measurable result, such as an inventory reconciliation or a pricing engine.
  2. Discover. Software Factory agents read the code, schemas, jobs, and interfaces, and draft the system map. The FDE checks it with the SMEs.
  3. Build the foundation layer. Read-only access, event capture, governed tools, and golden datasets.
  4. Pin the behavior. Turn the golden datasets into characterization tests. These tests record what the old system does today, right or wrong.
  5. Decide what to keep. The business owner marks each known defect in the rule register as "keep" or "fix". A matching output is not always a correct output.
  6. Rewrite one slice. Software Factory writes the replacement, one pull request at a time, through your CI. Each pull request must pass the build, the linters, and the characterization tests before a person reviews it.
  7. Prove it. Run old and new in parallel and compare every output. Prove the critical rules with property tests or a model checker.
  8. Cut over in steps. Move a small share of traffic first, keep a tested rollback, and widen when the numbers hold.
  9. Retire and hand over. Switch off the old component, measure the savings, and hand your team the code, the tests, and the evidence.

The first target matters. These are the ones we rank highest for a first project:

Target Usual language Why it is a good first project
Legacy integrations, SOAP to REST Go Clear contracts, easy to compare
Batch jobs and reconciliation pipelines Go Measurable outputs, often painful by hand
Old Java or .NET service extraction Same language or Go Bounded scope, existing tests to start from
Warehouse and ERP interfaces Go Wraps a stable core instead of replacing it
C or C++ modules that parse untrusted input Rust Memory safety bugs are the main risk
Oracle Forms screens TypeScript and Go Keep PL/SQL logic until its behavior is pinned
Full core banking or ERP replacement Mixed A poor first project for any team

A worked example: a distributor runs inventory on IBM i, with CSV exports, Excel macros, and nightly transfers. We would not replace the ERP. We would expose inventory and orders through a read-only API. Then we would rebuild the nightly reconciliation as a Go service and compare its results with the current process each night. Operations gets a dashboard of exceptions. The ERP stays. The manual work goes.

Choosing the Target Language: Same Language, Go, or Rust

The cheapest rewrite is often no language change. A Java 8 to current Java upgrade, or .NET Framework to modern .NET, keeps the libraries, the team skills, and most of the tests. Change the language only when the component gains something clear from it.

Legacy system Common in Our default
COBOL on IBM mainframes Banking, insurance, government Keep the core ledger at first. Add Go services around it
Java 6 or 8, Java EE, WebLogic Finance, telecom, retail Current Java, or Go for new services
.NET Framework, VB.NET Manufacturing, healthcare, insurance Modern .NET, or Go for new services
Oracle Forms and PL/SQL Manufacturing, logistics, finance TypeScript front end, Go services
SAP ECC with custom ABAP Manufacturing, distribution SAP-native tools first
IBM i (AS/400) with RPG Wholesale, logistics Go integration services, then bounded modules
C and C++ components Infrastructure, devices, trading Rust
Old Python batch jobs Data teams Go, with Rust for proven hot paths

When to choose Go

Go is our default for business services, integrations, batch workers, and APIs. It compiles fast, so an agent's edit-build-test loop is short. The language is small, so the code agents write is easy for people to review. The Go team names cloud services, networking, command-line tools, and DevOps tooling as core uses (go.dev/solutions).

Go is a memory safe language. CISA and its partner agencies list both Go and Rust as memory safe (CISA, The Case for Memory Safe Roadmaps). The gap is concurrency. A Go data race compiles without warning. The race detector finds races only on code paths that run, and it can raise memory use 5-10x and run time 2-20x (Go race detector). So we run it in tests and in the parallel run, not in production.

When to choose Rust

Rust is our choice for parsers of untrusted input, replacements for C and C++, low-latency message handling, and storage code. The evidence for C and C++ replacement is strong. Google estimates about 0.2 memory safety vulnerabilities per million lines of Android's Rust code. Its C and C++ code had close to 1,000 per million lines (Google Security Blog, Rust in Android, November 2025). The same post reports a rollback rate about 4x lower for medium and large Rust changes, and about 25% less time in code review.

Rust has costs. Builds are slower, the agent spends more turns on borrow checker errors, and fewer engineers can maintain it. Use it where its guarantees pay for that.

Why Rust works well with coding agents

The Rust compiler acts as a second reviewer that never gets tired. It rejects this code before it runs:

fn main() {
    let mut names = vec!["Alice"];
    let first = &names[0];

    names.push("Bob"); // error: cannot borrow `names` as mutable

    println!("{first}");
}

push can move the vector to new memory, which would leave first pointing at freed memory. In C++, the same mistake compiles and can become a use-after-free bug. In Rust, the agent gets an exact error and fixes it. Safe Rust also rejects data races at compile time (The Rust Programming Language, Fearless Concurrency).

The compiler has limits. It does not prevent deadlocks, memory leaks, or wrong business logic. Code that compiles can still pay the wrong claim amount. That is why the checks below go further than the compiler.

Every agent pull request on a Rust project runs these commands:

cargo fmt --all -- --check
cargo check --all-targets
cargo clippy --all-targets -- -D warnings
cargo test

We also give agents written rules:

  1. Do not add unsafe without a person's review.
  2. Do not silence a compiler or Clippy warning to make CI pass.
  3. Do not replace broken logic with a placeholder.
  4. Add a test for each requirement you implement.

Proving the Rewrite: Parallel Runs and Formal Verification

Two checks answer two different questions. A parallel run asks whether the new code matches the old code on real work. Formal verification asks whether the new code obeys a stated rule for every input. You need both, plus a business owner who confirms the rule is right.

Check What it proves What it misses
Characterization tests New code matches recorded old outputs Inputs nobody recorded
Parallel run New code matches old code on live traffic Rare inputs that do not occur during the run
Property tests A rule holds on many generated inputs Inputs the generator never makes
Model checking (Kani) A rule holds for every input inside the model's bounds Rules nobody wrote down
Business sign-off The rules are the right rules Nothing replaces it

The parallel run

In a parallel run, the old system stays the authority. Each request goes to both systems with the same correlation ID. The caller gets the old system's answer, and a comparator records every difference. GitHub's Scientist library works this way: it runs the old path and the new path, always returns the old result, and publishes mismatches (github/scientist).

How a parallel run works on top of the foundation layerLive inputs go to a routing layer, which sends each input to both the legacy system and the new service with the same ID. The legacy system answers the caller and writes data. The new service runs in shadow with no side effects. A comparator normalizes both outputs. A difference goes to a subject matter expert, who marks it keep or fix. When exit criteria are met, traffic shifts to the new service in steps. Everything sits on the foundation layer of read-only APIs, event capture, governed MCP tools, and golden datasets. Live inputs orders · claims · batch files Routing layer sends each input to both, with one correlation ID Legacy system The authority. Answers the caller and writes the data. COBOL · RPG · Java · PL/SQL New service Shadow mode. Same inputs, no side effects. Go or Rust · Kani-proven rules Comparator normalizes order, rounding, timestamps; logs real differences Differ Match SME decides Keep or fix. Agents patch the new service and the run repeats. Exit criteria met Shift traffic in steps: 5% → 25% → 100%, with rollback. Foundation layer read-only APIs · event capture · governed MCP tools · golden datasets
FIG 01The old system keeps answering. The new service earns traffic one step at a time.

Three problems break parallel runs. We plan for each one:

  1. Side effects. Do not charge a card, send an email, or move inventory twice. Scientist's own documentation says it "is only safe for wrapping methods that aren't changing data." For writes, the new system runs against an isolated copy, or it computes the result without applying it.
  2. False differences. The two systems can give the same answer in a different order, format, rounding, or timestamp. The comparator must normalize these and report only real differences.
  3. Different state. If old and new read different snapshots of the data, they disagree for reasons unrelated to the code. Record the input version and transaction boundary with each comparison.

Set the exit criteria before the run starts, or the run never ends. A typical set:

  • Zero unexplained differences in money, quantity, or status fields.
  • Coverage of at least one month-end and one quarter-end close.
  • Each known difference marked "keep" or "fix" by its business owner.
  • Peak-load performance at or better than the old system.
  • A rollback tested in production, with a named owner.

Formal verification for the critical rules

A parallel run only covers the inputs that occur. A model checker covers every input inside its model. Kani checks Rust code for panics, arithmetic overflow, undefined behavior, and the assertions you write. It either proves the property or returns a counterexample (Kani Rust Verifier).

A discount function shows the difference. This first version passes most unit tests:

pub fn discounted_price(price_cents: u64, percent: u8) -> u64 {
    price_cents - price_cents * percent as u64 / 100
}

Kani finds two faults. A price above u64::MAX / 100 can overflow the multiplication, and a percent above 100 underflows the subtraction. The fixed version and its proof:

pub fn discounted_price(price_cents: u64, percent: u8) -> u64 {
    let percent = percent.min(100) as u128;
    let discount = price_cents as u128 * percent / 100;
    (price_cents as u128 - discount) as u64
}

#[cfg(kani)]
mod proofs {
    use super::*;

    #[kani::proof]
    fn discount_never_raises_price() {
        let price: u64 = kani::any();
        let percent: u8 = kani::any();
        assert!(discounted_price(price, percent) <= price);
    }
}

kani::any() stands for every possible value of the type. The proof covers all 2^64 prices and all 256 percent values, not a sample. The same method proves other rules. Examples: "a claim payout is never more than the policy limit" and "an order never ships more units than are on hand." For a longer walkthrough, see formal verification for AI agent output.

For unsafe Rust, Miri finds undefined behavior such as out-of-bounds reads, use-after-free, and some data races. Miri tests only the paths that run, and its README states it "fundamentally cannot ensure that your code is sound" (rust-lang/miri). Go has no tool as mature as Kani, so for Go services we rely on the parallel run, property tests, and fuzzing.

Where each check is worth the cost

Workload Parallel run Formal verification Overall value
Ledgers and financial calculations Very high Very high for invariants Highest
Insurance claims and payouts Very high High Highest
Inventory reconciliation Very high Medium to high Highest
Pricing and discount engines Very high High Highest
ETL and data transformations Very high Selective High
Protocol parsers for untrusted input High Very high High
API modernization High Selective High
Internal CRUD screens Sometimes Rarely Low to medium
Front-end redesign Limited Rarely Low

For an ordinary CRUD app, integration tests and a staged rollout are enough. Save the heavy checks for code where an error costs real money.

The Evidence Report

Buyers do not pay for proofs. They pay to lower risk, lower maintenance cost, and change important systems faster. The proofs and the parallel run are how we show that. Each engagement keeps one evidence report up to date:

Section What it shows
Scope Which functions moved, and which stay on the old system
Comparisons Inputs compared, match rate, and every open difference with its owner
Decisions Each known defect, with the business owner's "keep" or "fix"
Proven rules Each formally verified property and its bounds
Test coverage Characterization, property, and integration tests, with results
Cutover readiness Exit criteria, status of each, and the rollback plan

Engineering leaders and auditors read the same document. Cutover becomes a review of evidence, not a vote of confidence.

The Business Case: One Module, Then the Program

Start with the unit economics of one bounded module. The numbers below are an example, not industry averages. Replace them with your own.

  1. Old system costs a year: $500k maintenance and support, $120k manual work, $50k infrastructure. Total: $670k.
  2. New system costs a year: $90k. Net savings: $670k - $90k = $580k a year.
  3. One-time cost: $220k for the foundation layer, rewrite, and verification, plus $50k for the parallel run and cutover. Total: $270k.
  4. Simple payback after cutover: $270k / ($580k / 12) = 5.6 months.

Savings start at cutover, not at kickoff. If the work takes six months, payback lands near month 12. One module earns about $1.2M of net cash in three years. That is a good project. It is not the reason a Fortune 2000 company modernizes.

The program compounds

A large enterprise runs hundreds of legacy applications, not one. The money is in a program that moves them in waves. Each wave costs less and moves more modules than the last, because it reuses what earlier waves built:

  • The foundation layer is already in place. Access, event capture, and governed tools are paid for once.
  • The rule register and golden datasets grow. Later modules share customers, products, and ledgers with earlier ones.
  • Software Factory reuses its harness. Comparators, test generators, and CI checks carry over from module to module.
  • The people know the process. SMEs know what a "keep or fix" decision needs, and security already approved the access pattern.
  • Savings fund the next wave. Each cutover adds monthly savings that pay for the modules still in flight.

This example program moves 21 modules in five waves. We assume each module saves $1.5M a year net, counting license and mainframe costs, support staff, and manual work. The foundation layer costs $1.5M, spent in the first six months. Each wave builds in the six months before its cutover.

Wave Cutover Modules Build cost per module Wave cost
1 Month 6 1 $0.80M $0.8M + $1.5M foundation
2 Month 12 2 $0.60M $1.2M
3 Month 18 4 $0.45M $1.8M
4 Month 24 6 $0.35M $2.1M
5 Month 30 8 $0.30M $2.4M
Total 21 $9.8M

The results, computed month by month:

  1. Peak cash outlay: $2.75M at month 12. The program spends $9.8M in total, but savings from earlier waves pay for most of the later ones.
  2. Break-even: month 23.
  3. Run-rate savings after the last wave: $31.5M a year (21 modules x $1.5M).
  4. Cumulative net cash: $24.0M at month 36 and $55.5M at month 48.
Cumulative net cash, 21-module program vs one module (US$ millions)21-module program in 5 wavesOne module only-$20M$0M$20M$40M$60MLast wave cuts overMonth 0Month 12Month 24Month 36Month 48Month 0 21-module program in 5 waves: $0.0MMonth 6 21-module program in 5 waves: -$2.3MMonth 12 21-module program in 5 waves: -$2.8MMonth 18 21-module program in 5 waves: -$2.3MMonth 24 21-module program in 5 waves: $0.9MMonth 30 21-module program in 5 waves: $8.2MMonth 36 21-module program in 5 waves: $24.0MMonth 42 21-module program in 5 waves: $39.7MMonth 48 21-module program in 5 waves: $55.5MMonth 0 One module only: $0.0MMonth 6 One module only: -$0.3MMonth 12 One module only: $0.0MMonth 18 One module only: $0.3MMonth 24 One module only: $0.6MMonth 30 One module only: $0.9MMonth 36 One module only: $1.2MMonth 42 One module only: $1.5MMonth 48 One module only: $1.8M$55.5M$1.8MBreak-evenExample inputs from the wave table in the post: $9.8M totalcost, $1.5M net savings per module per year from its cutover.Not industry averages.
CHARTCumulative net cash, 21-module program vs one module (US$ millions). Source: OpenNash worked example (model in the post).

The curve is flat for two years and then turns up hard. That shape is the reason to plan a program, not a project. A company that stops after the first module keeps the flat line.

The model behind the chart (Python)
# US$ millions. Cash per month, summed into a running total.
foundation = 1.5  # spent evenly over months 1-6
waves = [  # (cutover month, modules, build cost per module)
    (6, 1, 0.8), (12, 2, 0.6), (18, 4, 0.45), (24, 6, 0.35), (30, 8, 0.3)]
save = 1.5 / 12  # net savings per module per month, from the month after cutover

total = 0.0
for month in range(1, 49):
    cash = -foundation / 6 if month <= 6 else 0.0
    for cutover, modules, cost in waves:
        if cutover - 6 < month <= cutover:
            cash -= modules * cost / 6
        if month > cutover:
            cash += modules * save
    total += cash
    if month % 6 == 0:
        print(month, round(total, 1))

Two more effects are not in that figure. First, the foundation layer keeps its value after cutover, because agents and dashboards keep using it. Second, the expected cost of incidents falls. Say a reconciliation failure has a 5% yearly chance of costing $1M. If the new tests cut that chance to 1%, the expected loss falls by $40k a year. Across 21 modules, use your own incident history for those inputs.

When This Approach Is the Wrong Choice

  • The old system is stable and cheap. An IBM i system that runs orders reliably may need an API, not a rewrite.
  • The business cannot name an owner for the rules. Without someone to decide "keep" or "fix", the parallel run produces differences nobody can close.
  • The target is the whole ERP. Start with one interface or one pipeline. Earn the right to do the next one.
  • No one can get access. If security will not approve read-only access in the first weeks, fix that first. Nothing else moves without it.

Modernize Your Legacy Systems With OpenNash

Most companies that want value from AI first need systems that AI can see and use. OpenNash puts forward-deployed engineers on your team, and we work with Fortune 2000 companies. OpenNash Software Factory does the coding agent side of the work. It maps legacy code, pins its behavior with tests, and modernizes it through verified pull requests.

A typical engagement:

  1. Embed an FDE with your team, on site when possible, to interview SMEs and run the cross-functional meetings.
  2. Build the foundation layer: read-only access, event capture, governed MCP tools, and the business rule register.
  3. Pick one bounded target with a measurable result, such as a reconciliation pipeline or pricing engine.
  4. Rewrite it with Software Factory in the right language for the component, through your CI and code review.
  5. Prove it with a parallel run and formal verification of the critical rules, recorded in the evidence report.
  6. Cut over in steps with a tested rollback, retire the old component, and measure the savings.
  7. Hand over the code, tests, tools, and evidence to your team.

Book a call with an OpenNash forward-deployed engineer or email us at hello@opennash.com. In the first session, we review the legacy systems and data behind your top AI use cases. Then we show you which ones deliver the highest business value and the fastest ROI to fix first.