Skip to content
Aug 14, 2026·8 min read

Legacy system sizing beyond line count

Legacy system sizing needs more than line count. Measure decision depth, data fan-in, dead code, integrations, and observed production behavior.

Legacy system sizing beyond line count

A line count tells you how much source text exists. It does not tell you how much behavior a replacement must preserve, how many ways that behavior can branch, or how much of the source still runs. Treating those quantities as interchangeable is why apparently precise modernization estimates miss by a factor.

I have seen a small batch estate take longer to understand than a much larger application because every job touched the same mutable customer record and every exception surfaced in a printed control report. I have also seen intimidating directories collapse after reachability analysis showed that years of retired variants were still shipped in the source tree. The count was accurate in both cases. The conclusion drawn from it was wrong.

The useful unit is not a line. It is a piece of behavior that must be discovered, separated from its dependencies, implemented, and proved equivalent. Five measurements expose that work: decision depth, fan-in on persistent data, dead code share, integration surface, and the share of behavior known only through production output. None produces a magic price on its own. Together they give an engineering team a defensible shape of the system.

Line count measures inventory, not rewrite effort

Lines of code answer a narrow question: how much text did a particular counting rule classify as source? Robert Park's Software Engineering Institute report, Software Size Measurement: A Framework for Counting Source Statements, spends substantial effort defining physical lines and logical statements precisely. That is the first warning. Two tools can disagree before anyone has discussed comments, generated copybooks, expanded macros, embedded SQL, or multiple members that contain the same routine.

Even a perfectly normalized count measures inventory. It can help with repository ingestion, storage, parser throughput, or a rough first comparison among versions written in the same language under the same conventions. It cannot tell you whether 40 lines implement a straight mapping or a stateful rule with 16 paths. It cannot tell you whether 4,000 copied lines are live. It cannot tell you that one assignment to a status column changes eight downstream jobs.

Language also distorts the denominator. COBOL data declarations can make record layouts visually large. APL or SQL can express substantial behavior in a few statements. Generated Java can add thousands of repetitive accessors. A line-based ratio silently treats these as equivalent units of thought. They are not.

Do not repair this by applying a language conversion table, such as one COBOL line equals some number of Go lines. That recommendation remains popular because it produces a spreadsheet quickly and resembles historical productivity planning. It fails on modernization work because the target architecture should not preserve the source's textual shape. A shared copybook may become a schema and generated clients. Twenty near-identical batch programs may become one service plus configuration. A dense calculation may stay dense in Rust because the mathematics, not the source syntax, sets the work.

Keep the line count in the assessment, but label it honestly. Record physical lines, logical statements, generated lines, comments, and duplicated lines separately. Use them to explain the corpus. Never let their sum stand in for delivery effort or behavioral risk.

Cyclomatic depth shows where decisions become expensive

Cyclomatic complexity counts independent paths through a control-flow graph. Thomas McCabe introduced the measure in his 1976 paper, A Complexity Measure, with the graph expression commonly written as V(G) = E - N + 2P. It is useful because test obligations grow around decisions, not around formatting. The Software Engineering Institute's later technology description also gives an important qualification: a high value alone does not prove that a module is risky or should be redesigned.

For legacy assessment, the distribution matters more than the repository average. An average of six can describe a uniform codebase or a codebase where most routines are trivial and a few settlement modules contain hundreds of paths. Those estates need different plans. Report at least the median, the 90th percentile, the maximum, and the share of reachable routines above the threshold your team will inspect manually. Keep generated and dead routines out of the primary distribution, but report them beside it.

Cyclomatic complexity and cyclomatic depth are related but not identical. Complexity counts independent paths. Depth records how far decisions nest before control returns to a simpler level. A flat dispatch table with 30 cases can have high complexity and remain easy to partition. Five nested conditions that depend on prior mutations may have a lower path count yet be harder to explain, test, and move. Teams often blur the two, then wonder why a module with an acceptable score consumes the review budget.

Measure both per reachable routine. For depth, count nested conditional, loop, exception, and language-specific branch constructs after expanding preprocessing that changes control flow. Then inspect the routines that are high on either axis. The inspection should answer four questions: Does the branch depend on persistent state? Does it change data used later in the same transaction? Is the condition duplicated elsewhere? Can recorded inputs exercise both outcomes?

Avoid adding all complexity scores into one giant number. A sum rewards splitting a routine without reducing the system's actual decisions, and it hides concentration. Use a heat map or ranked table that retains module identity, calling context, and data touched. A rewrite estimate needs to know where decisions couple, not merely how many decision tokens a parser found.

Complexity also sets evidence work. A routine with one straight path may need representative boundary cases. A deeply nested routine that selects pricing, tax, or eligibility outcomes needs a matrix of observed and constructed cases. That does not mean every mathematical path deserves its own test. Infeasible paths exist, and some combinations are excluded by upstream validation. It means an estimator must fund the work to prove which paths matter instead of assuming line count already captured it.

Data fan-in reveals the real blast radius

Fan-in measures how many callers or flows converge on a component. On legacy systems, code-level fan-in is useful, but data-layer fan-in is often the sharper measure. Count the independently deployed programs, jobs, screens, reports, stored procedures, file feeds, and operator utilities that read or write each persistent record, table, file, queue, or shared data area.

The distinction between read fan-in and write fan-in matters. Fifty reports reading an append-only ledger create migration and compatibility work, but one reconciliation job writing historical rows may create a much harder cutover constraint. Mixed ownership is worse: an online transaction updates a master record, a nightly batch corrects it, and an operator utility can overwrite a field during an exception. A schema diagram shows the shared object. It does not show the order, authority, or operational reason behind those writes.

Start with static references, then reconcile them with runtime evidence. Static analysis can resolve direct SQL, known file names, copybook use, and literal calls. It will miss dynamic SQL, names assembled in variables, scheduler substitutions, aliases, exits, and access performed by tools outside the repository. Runtime database audit records, job logs, message metadata, and file catalog history expose some of that missing fan-in. Interview notes can add operator utilities, but treat recollection as a lead until evidence confirms it.

Rank data assets on more than the raw number of references. Separate readers from writers, online from batch access, and synchronous from deferred updates. Record whether a transaction boundary spans multiple assets. Mark fields whose meaning changes by program, such as a blank status interpreted as "pending" in one job and "not applicable" in another. Those semantic conflicts create more rewrite work than a clean table with many ordinary readers.

A practical fan-in record can stay compact:

asset,readers,writers,execution_modes,transaction_peer,observed
CUSTOMER-MASTER,14,4,online|batch,ADDRESS-HISTORY,yes
RATE-CONTROL,6,1,batch,none,no
CLAIM-QUEUE,3,3,online|operator,PAYMENT-FILE,partial

The last column is deliberate. A static reference and an observed access are different claims. Preserve both. When an estimator collapses them, an unexecuted reference can inflate scope while an unseen dynamic access can disappear.

High fan-in does not automatically mean "rewrite this first." It often means the opposite. A heavily shared data asset may need an explicit compatibility boundary, staged ownership transfer, or a period when old and new components coexist. The measurement changes sequencing because it shows where a locally correct change can still break the estate.

Dead code changes the denominator

Dead code share is the portion of the corpus that cannot execute in the defined production configuration. It should reduce implementation scope, but only after the team proves why the code is dead. Deleting a directory because nobody remembers it is not analysis.

Use three labels. Unreachable code has no path from a configured entry point. Unobserved code has a possible path but did not execute during the observation window. Retired behavior has an owner-backed decision that the replacement will not preserve. These labels cannot substitute for one another. IBM's documentation for identifying unreachable COBOL code explicitly says its result comes from static analysis and does not reflect the actual execution path. That limitation is exactly why static and dynamic evidence must stay separate.

Configuration defines reachability. A module unused in the weekday schedule may run at quarter close. A CICS transaction may be disabled in one region and active in another. JCL members may be selected through scheduler variables. A desktop executable may load a plugin named in a local configuration file that never reached source control. Build the entry-point set from production schedules, transaction definitions, deployment manifests, command procedures, registered jobs, and operator runbooks, not just a call graph rooted at the obvious main program.

Then calculate several shares: statically unreachable statements, reachable but unobserved statements, duplicated reachable statements, and behavior approved for retirement. Give each a confidence level and evidence reference. The estimate should exclude only the retired share. Unreachable code with uncertain configuration belongs in a resolution queue, while unobserved code still needs targeted tests or a business decision.

Dead code can still contain useful clues. An old branch may explain a field encoding or a report layout that survives elsewhere. Preserve the source and analysis record even when the new system omits the behavior. The mistake is paying to translate dead routines as if they were requirements. The opposite mistake is erasing them before the team has understood the live contracts that grew around them.

Integration surface is counted in contracts

Put a deadline on modernization
Every CodeHero rewrite is delivered in under 30 days, including systems that cross language boundaries.

An integration is not a single box on an architecture diagram. It is a contract with a transport, data shape, timing rule, error behavior, ownership boundary, security mechanism, and recovery procedure. Count those contracts, then measure how different they are.

One nightly fixed-width file can cost more to reproduce than ten ordinary HTTP endpoints. The file may require an exact name, code page, record length, sort order, trailer total, arrival window, rerun convention, and manual acknowledgement. A receiving party may parse undocumented filler bytes. None of that appears in a source line count. Much of it may not appear in the sending program either because the scheduler, transfer product, and operator procedure carry parts of the behavior.

Inventory every external edge and every internal edge that crosses an ownership or deployment boundary. Include databases owned by another team, inbound and outbound files, queues, remote procedure calls, terminal protocols, email, printer output, identity providers, hardware interfaces, spreadsheets used as import templates, and manual handoffs triggered by a report. Do not merge five files into "partner feed" if they have different schedules or failure handling.

For each contract, record direction, protocol, schema location, frequency, peak pattern, ordering, idempotency, retry rule, timeout, authentication, encryption, producer, consumer, test environment, and a sample captured from production. Unknown is a legitimate value. It is also work. A blank cell should never silently become an assumption that the target platform's default will match.

Integration surface has two useful scores. Contract count measures breadth. Contract novelty measures how many different mechanisms the team must reproduce or replace. Twenty files that share one generator and acknowledgment protocol may form one implementation family. Four interfaces using a mainframe queue, a printer control stream, a proprietary desktop automation hook, and a manually mounted share create four discovery and test problems.

Pay special attention to negative behavior. Consumers may depend on an empty file, a specific return code, a delayed retry, duplicate delivery, or the absence of a row. Happy-path samples rarely capture these contracts. Gather failure logs and rerun records. Ask operators what they do when the expected artifact does not arrive. Their action is often part of the system even though no compiler can see it.

Production output may be the only specification

Some legacy behavior exists only in what production emits. The code calculates it, users and downstream systems rely on it, but no current requirement explains it. That gap deserves its own measurement because it changes discovery and verification work.

Count behavior surfaces first: API responses, database changes, files, messages, screen fields, printed reports, audit records, return codes, timing events, and operator prompts. For each surface, classify the specification source as current documentation, executable tests, code inference, subject-matter confirmation, or production observation. Multiple sources can apply. The dangerous category is production-only: no trusted document or test defines the outcome, and people judge correctness by comparing what the old system produces.

Production-only does not mean mysterious forever. Capture representative input and output pairs, normalize volatile fields such as timestamps or generated identifiers, and replay the inputs through the old system under controlled conditions where possible. Preserve ordering, rounding, encoding, blank handling, and error output before anyone "cleans them up." A trailing space can be irrelevant in one report and a field boundary in another.

Measure this as a weighted share, not a raw output count. Give greater weight to outputs that move money, close books, control physical work, satisfy audit review, or feed another system. Also record coverage: how much input variety, calendar variety, and failure behavior the captured traffic contains. Thirty days of online requests may cover ordinary paths yet miss year-end processing. A million repeated health checks add almost no behavioral knowledge.

This is where estimation and verification meet. If behavior is documented and tested, the replacement team can implement against an explicit contract. If behavior lives only in output, the team must discover the contract, build a comparator, classify differences, and obtain a decision when old behavior is inconsistent. That work exists even when the responsible routine is only 80 lines.

Recorded traffic is evidence, not an oracle. It can contain bad outcomes, masked defects, sensitive data, and accidental dependencies. Apply access controls and minimization, identify fields that cannot leave the customer perimeter, and ask an accountable owner whether a mismatch exposes a regression or an old bug worth retiring. Automated parity without that decision process can preserve mistakes with impressive accuracy.

A sizing profile keeps unlike risks separate

Bring the whole legacy tree
The platform analyzes every language in the tree in parallel, including systems over a million lines.

Combine the measurements in a profile, not a universal weighted score. A single number looks convenient but destroys the information needed to choose architecture, sequence work, and set verification depth. Two systems can receive the same score while one has concentrated decision logic and the other has simple code behind dozens of brittle interfaces.

The profile should contain several linked records for the production configuration under assessment. The corpus record holds logical statements plus language, generated, duplicate, and comment shares. The decision record holds complexity and maximum nesting, with the median, 90th percentile, maximum, and reachable hotspots. The data record holds readers and writers per asset, transaction peers, execution modes, and observed access.

The reachability record holds retired and unresolved shares beside static roots, the runtime window, and owner decisions. The contract record holds distinct interfaces, mechanism families, failure samples, and test access. The behavioral evidence record holds weighted production-only surfaces, captured input variety, calendar gaps, and the owner responsible for mismatch decisions. Keep identifiers stable across these records so a reviewer can move from a hotspot to its evidence without matching descriptions by hand.

Version the profile with the entry points, configuration set, observation period, tool versions, and exclusions. Otherwise a later scan can appear to contradict the assessment when it merely used different scheduler roots or expanded copybooks differently.

Use bands rather than false precision. For decision depth, a band might distinguish ordinary routines, review hotspots, and decomposition candidates. For data fan-in, it might separate isolated assets, shared read models, and contested write ownership. Define each band in terms of action. A red cell should mean "requires a compatibility boundary and owner review," not "looks scary."

The profile also exposes uncertainty. Mark whether each value comes from static analysis, runtime observation, configuration records, or an owner decision. Attach an evidence reference and confidence. An unknown integration retry rule and an exact count of 200,000 lines should not average into a reassuring score. The unknown may dominate cutover risk.

Store raw observations beside derived values. If a scanner reports 26 callers while runtime records show 19, preserve both counts and the reconciliation status. Later work may prove that five callers belong to retired schedules and two calls are dynamic aliases. Replacing the earlier number destroys the audit trail and makes the estimate look more certain than the assessment ever was. The same rule applies to complexity normalization, duplicate detection, and traffic coverage.

For estimation, convert profile items into work packages that have observable completion conditions. A decision hotspot is complete when its behavior table, implementation, and parity cases are accepted. A data asset is complete when ownership, transaction behavior, migration rule, and consumers are accounted for. An integration is complete when happy paths, failures, retries, and operational handoff work in the target environment. A production-only surface is complete when captured cases compare cleanly or an owner approves each intentional difference.

This keeps multipliers local. A high-complexity routine affects the work package that owns it. It does not arbitrarily make every file transfer and screen twice as expensive. A poorly specified report adds discovery and comparison work to that output surface. It does not inflate dead code. Local factors are easier to challenge, revise, and verify.

Estimate the evidence units, not the replacement lines

Size the behavior, not lines
CodeHero reads the full source tree and tests the rewrite against recorded production traffic.

Once the profile exists, estimate units of preserved behavior and proof. Start with reachable capabilities, then split them where data ownership, integration contracts, or verification methods differ. The target line count is unknown and largely irrelevant. Architecture work may remove repetition, combine programs, or replace procedural plumbing with platform facilities.

For each unit, estimate four activities: discovery, target design and implementation, evidence construction, and acceptance. Discovery includes resolving dynamic calls, missing layouts, ownership, and production-only rules. Evidence construction includes traffic capture, fixtures, comparators, and expected failure cases. Acceptance includes difference review by someone authorized to decide whether old behavior remains required.

Do not hide uncertainty inside a larger effort number. Maintain an assumption register with a test and an owner. "RATE-CONTROL has no interactive writers" is testable through access records and operator review. "All quarter-close jobs appear in the scheduler export" is testable against execution history. Resolve high-impact assumptions early because they can change boundaries, not just hours.

A worked comparison makes the point. System A has 700,000 logical statements, 45 percent approved retired behavior, moderate decision depth, two write-heavy data hubs, and six integration families with captured failure cases. System B has 180,000 statements, almost no retired behavior, several deeply nested settlement routines, nine contested data assets, and outputs whose rules exist only in month-end reports. A line-count estimate makes A nearly four times larger. A behavioral profile can reasonably show that B carries more discovery and acceptance work. The profile does not prove the price; it shows why the price must follow evidence rather than text volume.

This method also makes vendor estimates comparable. Ask each bidder to return its counted entry points, unreachable classification, complexity distribution, high-fan-in assets, contract inventory, production-only surfaces, and unresolved assumptions. If one bidder gives a line multiplier and another identifies the nine assets with competing writers, you can see who has examined the system and who has priced a story.

CodeHero reads the whole source tree in parallel, then verifies modernized behavior against recorded production traffic with a parity harness; that combination addresses corpus scale and behavioral proof as separate problems. The distinction matters more than any claim about how many lines a tool can ingest.

Approval should follow the measurement trail

A credible legacy system estimate lets another engineer trace scope back to evidence. You should be able to select any major work package and find the entry points that reach it, the decisions it contains, the data it reads or writes, the contracts it crosses, and the production examples that define acceptance.

Before approving a plan, ask what the assessor excluded and under whose authority. Ask which runtime periods the observation covered, including quarter-end or year-end paths. Ask for the largest complexity hotspots rather than an average. Ask which data assets have multiple writers. Ask which interfaces lack failure samples. Ask which outputs have no specification beyond production. Direct answers can include uncertainty; vague confidence should stop the approval.

Line count still belongs on the first page because it describes the material being analyzed. It should sit beside the dead share and generated share, not above the measures that describe behavior. The estimate should change when a hidden writer appears, when a supposedly dead job proves active, or when an output lacks enough examples. If it changes only when someone finds another directory of source files, the model is measuring inventory and calling it engineering.

The first deliverable worth paying for is the versioned sizing profile and its evidence register. It creates a concrete basis for architecture and commercial decisions, and it gives the replacement team a definition of done that survives contact with production.

FAQ

Is line count ever useful for estimating a legacy rewrite?

Yes, but only as a corpus measure. It helps explain parser throughput, repository composition, and rough scale within comparable languages, but it cannot price behavioral discovery or acceptance by itself.

What is a better metric than lines of code?

No single metric replaces line count. Use a profile of reachable decision complexity, data-layer fan-in, retired code share, distinct integration contracts, and behavior supported only by production evidence.

How should cyclomatic complexity affect a modernization estimate?

Use its distribution to find routines that need deeper analysis and more test evidence. Do not sum every score or apply one repository-wide multiplier, because concentration and nesting matter more than a grand total.

What does data-layer fan-in mean?

It is the number and type of programs, jobs, screens, reports, and utilities that converge on a persistent data asset. Separate readers from writers and record transaction relationships, since contested writes usually constrain sequencing.

Can dead code be excluded from rewrite scope?

Exclude behavior only after an accountable owner approves its retirement. Statically unreachable or unobserved code still needs configuration checks, runtime evidence, or targeted tests before the estimate treats it as gone.

How do you count legacy system integrations?

Count distinct contracts, not diagram boxes. Record transport, schema, schedule, ordering, retry and error behavior, security, ownership, test access, and production samples for each boundary.

What if the legacy system has no reliable documentation?

Treat production input and output as evidence, then build normalized replay cases and comparators. Someone with authority must still decide whether each difference is a regression or an old defect that the replacement should drop.

How long should production traffic be recorded?

The right period covers behavioral variety, not an arbitrary number of days. Include ordinary traffic plus calendar events, batch cycles, exceptions, and failures; a long window full of repeated requests can still miss the important paths.

Can multiple legacy systems be compared with one sizing score?

A single score hides why systems differ. Compare dimension profiles and evidence confidence instead, then price the work packages affected by each hotspot or unknown.

What should a legacy assessment vendor deliver before a quote?

Ask for counted entry points, reachability classes, complexity distributions, high-fan-in data assets, integration contracts, production-only behavior surfaces, and unresolved assumptions. Every major scope claim should point to evidence and a production configuration.