Skip to content

Modernizer App Architecture

The ModernizeSpec SDK does not live in your legacy app or your new app. It lives in a separate helper project — the Modernizer App — that orchestrates analysis, parity testing, and progress tracking between the two systems.

Every ModernizeSpec migration involves three distinct codebases:

Legacy App (Read-Only)

The existing system being modernized. Never modified by the SDK. Analyzed via static analysis, runtime profiling, and database schema inspection. The Modernizer reads from it but never writes to it.

Modernizer App (Orchestrator)

A separate project/repo that contains @modernizespec/* packages. Reads the legacy codebase, produces spec JSON files, runs parity tests against both systems, tracks migration progress, and collects runtime profiling data. This is where the SDK lives.

New App (Clean)

The target system being built. Has no ModernizeSpec dependencies. Gets validated against the legacy system via parity tests run by the Modernizer. Stays clean and focused on business logic.

No. Adding the SDK to the legacy system means modifying the codebase you are trying to replace. This introduces risk of side effects, dependency conflicts with old package managers, and muddies the line between “understanding the system” and “changing the system.” The legacy app should remain untouched — a stable reference for parity verification.

The Modernizer App acts as the intermediary between the legacy and new systems. Here is the sequence of operations:

The Modernizer reads the legacy source code and produces spec files:

Terminal window
npx modernizespec analyze --legacy-path ../legacy-app

This generates domains.json, complexity.json, and other spec files based on static analysis of the legacy codebase.

Using the spec files as a guide, the team builds the equivalent modules in the new app. The Modernizer does not generate code — it provides the map. The new app is built independently.

The Modernizer records how the legacy system responds to requests:

import { captureEndpoint } from '@modernizespec/sdk';
// Record legacy behavior as baseline
const baseline = await captureEndpoint("GET /api/invoices/123", {
target: "legacy",
outputDir: "./baselines/invoices/"
});

Baselines are stored in the baselines/ directory as JSON snapshots.

The Modernizer sends the same requests to both systems and compares:

import { compareBehavior } from '@modernizespec/sdk';
const diff = compareBehavior(baseline, modern, {
ignoreFields: ["timestamp", "request_id"],
tolerance: { amount: 0.01 }
});

Results update parity-tests.json automatically.

The Modernizer ingests runtime data (traces, metrics) from the legacy system and produces runtime-profile.json. This reveals hot paths, dead code, and database query patterns.

As contexts are extracted and parity tests pass, the Modernizer updates migration-state.json with current progress, velocity metrics, and blockers.

Parity checking is the mechanism that proves the new system behaves identically to the legacy system. The Modernizer App orchestrates this in five phases.

Run requests against the legacy system and record responses:

import { captureEndpoint } from '@modernizespec/sdk';
const baseline = await captureEndpoint("GET /api/invoices/123", {
target: "legacy",
outputDir: "./baselines/invoices/"
});
// baseline contains: status, headers, body, timing

Implement the same endpoint in the new app. The new app has no awareness of baselines — it just implements the business logic.

Run the same request against the new system and diff the responses:

import { captureEndpoint, compareBehavior } from '@modernizespec/sdk';
// Capture from new system
const modern = await captureEndpoint("GET /api/v2/invoices/123", {
target: "new"
});
// Diff field-by-field
const diff = compareBehavior(baseline, modern, {
ignoreFields: ["timestamp", "request_id", "server_version"],
tolerance: { amount: 0.01 }
});

Table-driven tests verify field-level parity:

import { assertParity } from '@modernizespec/sdk';
// Fails if any field diverges beyond tolerance
assertParity(diff, {
strictFields: ["invoice_number", "line_items", "tax_amount"],
toleranceFields: { total: 0.01, discount: 0.005 },
ignoredFields: ["created_at", "updated_at"]
});

Results flow back into the spec files:

import { updateParityResults } from '@modernizespec/sdk';
await updateParityResults("./agents/modernization/parity-tests.json", {
context: "Invoices",
passed: 42,
failed: 3,
skipped: 0,
timestamp: new Date().toISOString()
});

Runtime profiling adds evidence from the live legacy system. Instead of guessing which code paths matter, you measure them.

Legacy App (instrumented) → APM / OpenTelemetry → Export → Modernizer → runtime-profile.json
  1. Instrument the legacy system with OpenTelemetry, Datadog, New Relic, or any APM tool
  2. Export traces and metrics to the Modernizer’s profiling/ directory
  3. Process the data with the SDK to produce runtime-profile.json
import { processTraces } from '@modernizespec/sdk';
const profile = await processTraces({
tracesDir: "./profiling/traces/",
outputFile: "./.agents/modernization/runtime-profile.json",
period: { start: "2026-01-01", end: "2026-01-31" }
});
Data PointImpact on Migration
Hot paths (high call count)Prioritize these for parity testing
Dead code (zero calls in production)Skip extraction — migrate last or never
N+1 query patternsFix during extraction, not before
Slow endpointsOpportunity for improvement in new system
Resource usage spikesCapacity planning for the new system

Runtime profile data influences other spec files:

  • complexity.json — Hotspot rankings incorporate actual usage data, not just static analysis
  • extraction-plan.json — Most-used paths get extracted first
  • parity-tests.json — Hot paths get higher test coverage

See runtime-profile.json for the full file specification.

Create a Modernizer App alongside your existing project:

Terminal window
mkdir my-modernizer && cd my-modernizer
npm init -y
npm install @modernizespec/sdk @modernizespec/cli
npx modernizespec init --legacy-path ../legacy-app

This scaffolds the directory structure, analyzes the legacy codebase, and produces initial spec files. From here, iterate: capture baselines, build new modules, run parity tests, track progress.