What belongs in a software rewrite estimate?
A software rewrite estimate should measure code volume, branching, integrations, data, operations and the test surface, with evidence for each.

A rewrite estimate should describe uncertainty in behavior, not reward whoever counted the repository fastest. Lines of code matter because someone must inspect and replace them, but line count is only one input. Branching depth, external integrations, data semantics, operational jobs, and the surface available for parity testing often move the estimate more than raw size.
I have seen compact billing engines take longer to replace than sprawling reporting applications. The small system hid rules in nested branches, stored intermediate state in tables nobody documented, and called services that behaved differently at month end. The larger one repeated plain CRUD patterns and had clean request logs. Any estimate that begins and ends with "300,000 lines" puts a precise label on an unmeasured job.
What line count can tell you
Line count is a useful measure of inspection volume, but it does not measure rewrite difficulty by itself. A team still needs a reproducible count because casual numbers such as repository size, file count, or executable lines get mixed together during sales conversations.
Define the counting rule before comparing proposals. At minimum, separate production source, generated source, vendored dependencies, tests, database code, job control, configuration, and comments. A million lines that include generated client stubs is a different system from a million handwritten lines of COBOL, JCL, SQL, and copybooks. If an estimator will not show those categories, the total is not auditable.
A simple inventory command gives engineering leaders a first check. cloc is not an estimator, but it reports a stable shape that can be rerun after exclusions change:
cloc . --exclude-dir=vendor,node_modules,dist --by-file --json --out=cloc.json
jq '.SUM | {blank, comment, code}' cloc.json
The result has this form:
{"blank":18420,"comment":27116,"code":263904}
Keep the file level output too. It lets a reviewer find concentrated subsystems, generated blocks that escaped the exclusions, and languages the first pass missed. For a mainframe estate, count JCL, copybooks, assembler exits, SQL procedures, screen maps, and scheduler definitions alongside the application language. They may contain little business logic, yet they define how the system starts, stops, exchanges files, and recovers.
Line count works best as a denominator. Defects per thousand lines, branches per module, integration calls per thousand lines, and tests covering each business capability all tell you something. A price created by multiplying lines by a universal rate does not. The same line can be a field declaration, a generated getter, or the branch that decides whether an account receives interest after a backdated adjustment.
Branching measures the behavior you must preserve
Branching matters because every decision path can encode a distinct observable behavior. Count decision points, measure how they combine, and identify where state or external data changes the result. A flat module with twenty independent validations is easier to reason about than a routine where ten decisions nest inside one another and share mutable state.
Cyclomatic complexity is a reasonable first signal. Thomas McCabe defined it from the control flow graph, commonly summarized for one connected routine as decisions plus one. The number does not predict effort on its own. It tells you how many independent paths exist, while nesting tells you how difficult those paths are for a person or tool to follow.
The distinction between branch count and branching depth is routinely blurred. Suppose two routines each contain eight decisions. In the first, eight guard clauses reject invalid records and then one calculation runs. In the second, eligibility, jurisdiction, product class, effective date, exception status, and prior adjustments nest six levels deep. Both may receive a similar cyclomatic score. The second usually demands more fixtures, more careful state reconstruction, and more review because a condition changes the meaning of every condition below it.
Ask an estimator for a distribution, not an average. A repository average hides the five routines that control money movement or plant operation. Useful buckets include routines above an agreed cyclomatic threshold, maximum nesting depth, fan in, fan out, and the amount of code reachable from high consequence entry points. The threshold should flag inspection work, not declare code "bad."
Also ask whether the analysis resolves dynamic dispatch, generated calls, macros, and database driven rules. Static analysis can miss a program name read from a table, a COBOL CALL assembled at runtime, or a VB6 form event wired outside the visible routine. An honest report labels unresolved edges. It does not quietly treat missing graph data as low complexity.
Complexity changes an estimate through evidence work. Each consequential path needs a known input, expected output, and relevant starting state. If production traces cover most paths, the team can derive cases from reality. If logs capture only a final status code, someone must reconstruct the decisions through source reading and targeted runs. That is a different amount of work even when the code is identical.
External integrations create boundary risk
External integrations deserve their own inventory because a rewrite fails at boundaries more often than at syntax. An "integration" includes more than an HTTP API. It includes files dropped on shared storage, message queues, terminal sessions, database links, printer streams, SMTP, scheduler events, shell commands, identity providers, hardware devices, and people who manually move output between systems.
Count each distinct contract, then record its direction, protocol, owner, frequency, authentication, data shape, failure behavior, and test substitute. Ten endpoints behind one documented service client can be simpler than one nightly fixed width file whose owner left and whose rejected records appear in an operator mailbox.
The awkward question is whether an integration can be exercised outside production. A vendor sandbox with artificial data may not reproduce throttling, ordering, certificate rollover, or end of day behavior. A shared database may have no test copy. A partner may accept only one certification window. Those constraints belong in the estimate because they determine how quickly the team can learn whether the replacement is correct.
Inventory both sides of each boundary. Calling an API is only half the contract. The legacy system may also retry on specific codes, suppress duplicates, depend on response ordering, or write a reconciliation record after a timeout. File interfaces carry similar rules in naming, encoding, line endings, trailers, empty files, partial delivery, and reruns. Treating them as "one SFTP integration" throws away the details most likely to stop cutover.
Ownership is a schedule input, not an organizational footnote. Mark who can answer questions, issue credentials, approve a firewall rule, provide a sample, and observe a test. If no owner exists, price discovery and contingency explicitly. Do not hide that risk inside a generic project buffer, because leaders need the option to assign an owner before signing.
A credible estimate distinguishes integration implementation from integration proof. Writing a new client may be routine. Proving that it behaves correctly during retries, malformed inputs, duplicates, late delivery, and partner downtime is where the uncertainty sits. Ask for both numbers.
The test surface sets the confidence level
The test surface is the set of observable behaviors that can be driven and compared, not the number of test files in the repository. Existing unit tests help only when they assert behavior that the new system must preserve. A suite full of mocks may describe the old class structure while saying little about invoices, postings, messages, or files at the system boundary.
Measure the surface by business capability and observation point. For each entry point, record which inputs can be replayed, which starting state can be recreated, which outputs can be captured, and which side effects can be compared. Include API responses, database mutations, outbound messages, generated files, ledger entries, permissions, timings that affect ordering, and operator visible errors.
Recorded production traffic is especially useful because it contains combinations nobody remembered to put in a test plan. It still needs handling rules. Secrets and personal data may require redaction, requests may depend on expired state, and replaying a command may trigger an external side effect. A trace is evidence, not automatically a safe fixture.
Coverage has at least three meanings in a rewrite, and proposals often swap them without warning. Source coverage asks which old statements or branches executed. Requirement coverage asks which documented rules have tests. Behavior coverage asks which externally visible input and output combinations have been compared. A rewrite can report high source coverage while missing an undocumented file convention that operations depends on. For acceptance, behavior coverage carries the most weight.
Ask how mismatches will be classified. Some differences are defects. Some expose an old bug the business wants preserved temporarily. Others come from timestamps, generated identifiers, ordering, rounding, locale, or nondeterministic dependencies and need normalization. Without an agreed comparison policy, a parity percentage is meaningless because a team can improve it by ignoring difficult fields.
The estimate should state the proposed evidence level. A low consequence internal lookup tool may need representative cases and user acceptance. A posting engine may need replay across recorded traffic, branch focused fixtures, reconciliation totals, and controlled failure injection. The target confidence changes the work. "Rewrite the same code" does not define that target.
Data semantics can outweigh application size
Data work grows with meaning, history, and coupling, not just row count. A small database can contain overloaded columns, implicit codes, broken foreign keys, temporal rules, and stored procedures that carry most of the system's behavior. A large append only event table may move cleanly because its contract is simple.
Estimate schema translation, data cleanup, migration execution, reconciliation, and rollback separately. These are different jobs. Translating a packed decimal field into a Postgres numeric type is mechanical. Deciding whether blank, zero, and a sentinel value all mean "unknown" requires evidence from code and production data. Reconciliation then proves that the chosen mapping preserved balances and counts.
Hidden state is particularly expensive. Legacy programs often communicate through work tables, control records, sequence files, environment variables, or naming conventions rather than explicit calls. A batch job writes a status byte; a later job interprets it as permission to skip an account. A schema diagram will not show that behavior. Dependency analysis must include reads and writes, job order, and the lifespan of intermediate state.
Volume still matters, but ask for distributions and operational limits. Peak daily change, largest partition, record width, retention, late arriving records, and allowable outage matter more than a lifetime row total. A migration that fits in one maintenance window has a different plan from one that needs change capture and repeated reconciliation while both systems run.
Do not accept "database migration included" as a measurement. Ask which tables have source to target mappings, which fields have unresolved meanings, which stored routines move into services, how many reconciliation rules exist, and how rollback works after writes reach the new system. An estimator who cannot answer those questions has priced an assumption.
Operational code belongs inside the system boundary
Schedulers, deployment scripts, operator runbooks, access rules, monitoring, and recovery procedures are part of the application behavior. Leaving them outside the estimate produces a replacement that passes a demonstration but cannot close a business day.
Batch estates make this obvious. JCL or CL may define dependencies, conditional execution, dataset allocation, restart points, and notifications. The application source can look straightforward while the job network carries the true control flow. On desktop systems, installer scripts, registry settings, shared folders, and scheduled tasks play the same role. On web monoliths, cron entries and manual admin actions often fill the gap.
Ask for a count of scheduled jobs, triggers, deployment units, environment specific settings, roles, alerts, reports, and documented operator interventions. Then connect them to business capabilities. A list with no dependency graph cannot show whether a failed job can restart safely or whether one credential blocks fifteen processes.
Recovery behavior needs direct tests. Kill a batch after it writes half its output. Deliver the same message twice. Make a dependency time out after accepting a request. Restore a database snapshot with queued work still pending. The old system may contain years of practical answers to these cases, even if nobody wrote them down. The rewrite must either preserve those answers or replace them with decisions the business approves.
This area also exposes a popular but wrong recommendation: "modernize operations after functional parity." Teams like it because it appears to reduce scope. It actually postpones discovery of restart, ordering, access, and monitoring requirements until the new design has hardened. You can defer cosmetic dashboards. You cannot defer the behavior that keeps money, records, and jobs consistent after failure.
Operational readiness should appear as measured work with owners and acceptance evidence. If a proposal treats it as a short line near deployment, the price is incomplete.
Architecture changes need two separate estimates
Modernizing the architecture and preserving behavior are related workstreams, but they are not the same work. Price them separately so a design choice cannot quietly lower the promised evidence. A service boundary may improve ownership and deployment, yet it also creates contracts, failure modes, and data consistency decisions that the monolith handled with local calls and one transaction.
Transliteration estimates often look cheap because they map one old unit to one new unit. That approach can preserve accidental structure, global state, and obsolete deployment constraints. A serious rewrite should identify capabilities and choose boundaries that fit the target environment. The estimate must include the analysis needed to separate those capabilities, not just the production of equivalent syntax.
The opposite mistake is architecture ambition with no behavior budget. A proposal may promise services, events, a new client, and a new database while allocating little time to discover what the existing system actually does. That team will make clean design decisions against an incomplete model. The result can be attractive in a diagram and wrong during a refund, rerun, or partial failure.
Ask for two linked maps. The behavior map connects old entry points, decisions, state changes, and outputs. The target map assigns those behaviors to new components and states which old coupling will disappear. Every moved responsibility should have evidence on both sides: what proves the old behavior and what proves the new contract. This is how leaders can tell deliberate modernization from a file conversion with new directory names.
Cross cutting behavior needs special attention during decomposition. Authentication, authorization, transaction scope, idempotency, rounding, locale, audit records, and error mapping may appear in many old modules because no central boundary existed. Counting each copy as a separate feature inflates the estimate. Counting the concern once and ignoring its many observable variants understates it. Measure the distinct behaviors, then design the shared implementation.
Performance requirements belong in the same comparison. Do not copy every accidental timing characteristic of the legacy system, but identify deadlines that carry business meaning: a terminal response before an operator retries, a batch completed before the next market opens, or an export delivered before a partner cutoff. Record current distributions when evidence exists and define target limits. A vague promise that the new system will be faster cannot support acceptance.
Architecture also changes the cutover plan. A single replacement event requires full parity and a credible rollback before traffic moves. Incremental replacement requires routing rules, coexistence data flows, and proof that old and new components agree during the transition. Neither approach is universally cheaper. The estimate should show the temporary machinery each approach needs and when that machinery can be removed.
When reviewers see behavior preservation and target design as separate rows, tradeoffs become honest. They can simplify a target boundary without pretending an old rule vanished, or retire an old rule through an explicit business decision. That is the control an engineering leader needs before accepting a fixed number.
A weighted model makes assumptions visible
A useful software rewrite estimate combines measured dimensions and shows how each one affects effort. It does not need a universal formula. It needs a model that reviewers can challenge, update, and connect to evidence.
Start with an inventory table at subsystem level. One row per deployable unit or coherent business capability is usually more informative than one row per repository. Use columns such as these:
| Dimension | What to record | Why it changes work |
|---|---|---|
| Source volume | Handwritten production code by language | Sets inspection and replacement volume |
| Control flow | Complex routines, nesting, unresolved calls | Sets path discovery and fixture work |
| Boundaries | Contracts, owners, test substitutes | Sets coordination and failure testing |
| Data | Mappings, hidden codes, migration mode | Sets transformation and reconciliation |
| Test surface | Replayable inputs and comparable outputs | Sets evidence creation and acceptance work |
| Operations | Jobs, restart points, roles, alerts | Sets production readiness work |
Score confidence beside every measurement. "42 interfaces, 39 inspected, 3 inferred" is more useful than "42 interfaces." Record the source of evidence, such as static analysis, production trace, configuration scan, interview, or sample data. An inferred item should carry more contingency than an observed one.
Then express the estimate as ranges by workstream. A simple internal model might look like this:
replacement = source inventory adjusted for repetition and generated code
behavior proof = consequential paths x fixture cost x evidence gap
integration work = contract implementation + failure proof + owner delay risk
data work = mapping + transformation + rehearsal + reconciliation
operations = deployment + observability + recovery exercises
Do not turn that sketch into fake arithmetic. The multipliers must come from the delivery team's own completed work, and the units need definitions. Its purpose is to expose why two similarly sized systems receive different estimates.
Ranges should narrow as evidence improves. Before code access, a proposal may carry broad bounds and explicit assumptions. After repository analysis, traffic sampling, and integration interviews, the supplier should replace assumptions with counts. If the number stays fixed while the evidence changes, the original estimate was probably a commercial target rather than an engineering result.
CodeHero reads the whole codebase across languages and uses recorded production traffic in a parity harness, so its estimate can tie source structure to observable behavior instead of applying one rate to every line. That still does not excuse unclear inputs: engineering leaders should ask to see what was counted, what traffic represents, and which boundaries remain unproved.
Watch an estimate fail in a small system
Consider a 38,000 line claims pricing application. A line based quote makes it look modest. The repository contains a desktop client, a calculation library, SQL procedures, and a nightly export. Existing tests cover the calculation library, and the team initially treats the rest as ordinary plumbing.
Discovery finds four facts. The client chooses calculation paths by enabling and disabling fields before it calls the library. Product rules live in six SQL tables maintained by operations. The nightly export is accepted only when its trailer totals match a partner's independent calculation. Repricing a historical claim depends on the rule table version that was active on the original service date.
None of those facts adds many source lines. Each one expands the behavior that must be captured. Client state becomes an input contract. Rule tables need versioned fixtures and migration rules. Trailer logic needs partner examples and rejection tests. Historical repricing needs time based data reconstruction.
Now suppose the current test suite executes 85 percent of the calculation library. That number sounds reassuring, but it covers only the component whose inputs the client already transformed. A parity plan must capture the user action, client state, selected rule version, database changes, calculation output, and export record. The useful test surface is end to end, even if the target architecture separates those concerns cleanly.
The estimate changes because the unit of work changed from lines replaced to behaviors proved. The team may still rewrite only 38,000 lines. It also needs to identify UI decision paths, extract rule history, emulate partner validation, and compare old and new outputs across recorded cases. Calling that an overrun would be dishonest if the original quote never measured those items.
There is a practical response when discovery reveals this shape. Freeze the fixed price until the supplier produces the boundary inventory and parity plan. You do not need every test written before contract signature, but you need counts, evidence sources, exclusions, and a method for turning unknowns into decisions. Otherwise the contract transfers an unknowable risk on paper while leaving both parties to fight about it later.
Ask for the measurement pack before signing
An engineering leader should receive the estimate and the evidence behind it. A polished total without the measurement pack prevents technical review and makes later scope arguments almost inevitable.
The pack should answer a compact set of questions:
- What source categories were counted, what was excluded, and can we rerun the inventory?
- Where are the deepest and most consequential decision paths, including unresolved dynamic calls?
- Which external contracts exist, who owns them, and which can be tested outside production?
- Which data meanings, migrations, and reconciliation rules remain unresolved?
- What inputs can be replayed, what outputs can be compared, and how are acceptable differences normalized?
Ask for the answers by subsystem, with confidence labels and evidence sources. A repository wide average is not enough. One untestable settlement interface can dominate the risk while fifty ordinary modules make the average look safe.
The commercial terms should follow the measurements. Fixed scope works when boundaries and acceptance evidence are known. A paid discovery phase can make sense when access, owners, or production traces are missing, but it should produce reusable artifacts rather than a slide deck: inventories, graphs, mappings, samples, and an updated range. Contingency should attach to named unknowns and shrink when the customer resolves them.
Reject estimates that offer precision without exclusions. Also reject the opposite move, where a supplier calls everything uncertain and asks for unlimited time. Good estimating makes uncertainty smaller by inspecting code, tracing behavior, and testing boundaries. It shows which unknowns remain and who can remove them.
CodeHero commits to delivery in under 30 days, which makes early measurement of code, behavior, integrations, data, and operations essential rather than optional. Whatever supplier you consider, insist that the estimate names the testable system they intend to deliver. A line total describes the material on the floor. The measurement pack describes the building they are responsible for finishing.
FAQ
How accurate is a rewrite estimate based on lines of code?
It is accurate only as a measure of source inspection volume. The price can be wrong by a large factor when a small codebase has deep branches, hidden data rules, weak tests, or boundaries that cannot be exercised outside production.
What should be excluded from a code line count?
Report generated source, vendored dependencies, comments, tests, configuration, and production code separately rather than deleting them from one total. The exclusions must be documented and reproducible because generated code and operational configuration still affect parts of the rewrite.
Does cyclomatic complexity predict rewrite cost?
No single complexity score predicts cost. Use cyclomatic complexity to locate independent paths, then inspect nesting, mutable state, dynamic calls, and business consequence to decide how much discovery and parity evidence those paths need.
How do external integrations affect a rewrite quote?
Each boundary adds contract work, failure handling, coordination, credentials, and proof. One poorly documented file exchange with no test endpoint can cost more to validate than several ordinary APIs, so the quote should show implementation and testing separately.
What is the test surface of a legacy system?
The test surface is the set of inputs you can drive and outputs or side effects you can compare. It includes requests, files, database changes, messages, reports, errors, and operational outcomes, not merely the unit tests stored with the source.
Can production traffic be used to test a rewrite?
Yes, recorded traffic can supply realistic parity cases when the team redacts sensitive data, reconstructs required state, and blocks unsafe side effects. It does not replace targeted tests for rare branches, failures, time boundaries, or cases absent from the recording window.
Why do data migrations make small rewrites expensive?
Effort follows data meaning and coupling rather than database size. Overloaded fields, historical rule versions, stored procedures, invalid records, and strict reconciliation can create substantial analysis and proof work even when the row count is modest.
Should operational scripts be included in rewrite scope?
Yes. Schedulers, deployment scripts, access rules, alerts, restart points, and operator procedures determine whether the replacement can run and recover. Leaving them for after functional parity postpones requirements that can change the architecture.
What evidence should accompany a fixed rewrite price?
Ask for rerunnable source counts, complexity distributions, boundary and data inventories, a behavior comparison plan, operational scope, exclusions, confidence labels, and named unknowns. The price should trace back to these artifacts by subsystem.
When is paid discovery reasonable before a rewrite?
Paid discovery is reasonable when the supplier lacks code access, production traces, integration owners, or representative data. It should end with reusable technical artifacts and a narrower estimate, not a presentation that leaves the original unknowns untouched.