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:
- Interview the SMEs. Ask each expert to walk through real cases, including the exceptions they fix by hand.
- Run the cross-functional meetings. Put finance, operations, IT, and security in one room to agree on rules, owners, and edge cases.
- Turn answers into artifacts. Each meeting ends with updates to the rule register, the golden datasets, or the acceptance criteria.
- Get access. Work with security and IT to approve read-only access, service accounts, and audit logs.
- 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).
- 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.
- Discover. Software Factory agents read the code, schemas, jobs, and interfaces, and draft the system map. The FDE checks it with the SMEs.
- Build the foundation layer. Read-only access, event capture, governed tools, and golden datasets.
- Pin the behavior. Turn the golden datasets into characterization tests. These tests record what the old system does today, right or wrong.
- 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.
- 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.
- Prove it. Run old and new in parallel and compare every output. Prove the critical rules with property tests or a model checker.
- Cut over in steps. Move a small share of traffic first, keep a tested rollback, and widen when the numbers hold.
- 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:
- Do not add
unsafewithout a person's review. - Do not silence a compiler or Clippy warning to make CI pass.
- Do not replace broken logic with a placeholder.
- 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).
Three problems break parallel runs. We plan for each one:
- 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.
- 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.
- 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.
- Old system costs a year: $500k maintenance and support, $120k manual work, $50k infrastructure. Total: $670k.
- New system costs a year: $90k. Net savings: $670k - $90k = $580k a year.
- One-time cost: $220k for the foundation layer, rewrite, and verification, plus $50k for the parallel run and cutover. Total: $270k.
- 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:
- 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.
- Break-even: month 23.
- Run-rate savings after the last wave: $31.5M a year (21 modules x $1.5M).
- Cumulative net cash: $24.0M at month 36 and $55.5M at month 48.
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:
- Embed an FDE with your team, on site when possible, to interview SMEs and run the cross-functional meetings.
- Build the foundation layer: read-only access, event capture, governed MCP tools, and the business rule register.
- Pick one bounded target with a measurable result, such as a reconciliation pipeline or pricing engine.
- Rewrite it with Software Factory in the right language for the component, through your CI and code review.
- Prove it with a parallel run and formal verification of the critical rules, recorded in the evidence report.
- Cut over in steps with a tested rollback, retire the old component, and measure the savings.
- 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.