Back to Home

Unified calculation engine for Next.js and Flutter

Article about how the author synchronized calculation logic between a Next.js website and a Flutter mobile app. Describes an approach using JSON specs as a single source of truth, code generation, and parity tests to ensure identical results.

How one JSON file saved two runtimes from discrepancies
Advertisement 728x90

Unified Calculation Engine for Next.js and Flutter: How to Avoid Discrepancies in Numbers

When the same construction calculator exists both as a Flutter mobile app and a Next.js website, even minor differences in formulas can undermine user trust. A real-world case showed: if the app and site output different concrete volumes for a fence post, it's not just a bug—it's a systemic issue of duplicated logic. The solution—a single source of truth based on declarative JSON specs and parity tests.

Problem of Two Independent Codebases

Initially, the calculation logic was implemented twice: in Dart for the Flutter app and in TypeScript for the Next.js site. Both projects evolved in parallel but independently. Coefficients, defaults (like hole diameter for a post), rounding rules, and material packaging lived separately in each runtime's code. The result: discrepancies in outputs for the same inputs.

This architecture violates the DRY (Don’t Repeat Yourself) principle and creates technical debt that eventually surfaces through user complaints. It's especially critical for niche tools where calculation accuracy directly impacts product trust.

Google AdInline article slot

Declarative Approach: JSON as the Source of Truth

Instead of duplicating business logic, all variable parameters were moved to structured JSON files—one per calculator. Each spec contains:

  • Defaults for input fields (e.g., pile diameter—150 mm)
  • Rounding rules (always up)
  • Loss coefficients depending on accuracy mode
  • Material packaging info (25 kg bags, 0.01 m³ yield per bag)
  • Minimum layer thicknesses and fastener spacings

These JSON files became the single source of truth. Now, any parameter changes are made once—in JSON—and automatically applied in both runtimes.

Code Generation for Synchronization

To ensure both projects use the same data, a code generation process was introduced:

Google AdInline article slot
  • On JSON spec changes, the npm run sync:specs script runs
  • The script reads all specs and generates a Dart file with a constant map (const Map)
  • The generated file is committed to the Flutter app repository
  • CI/CD for both systems checks for changes and deploys updated versions

The generated file is marked as @generated, and manual edits are blocked by the linter. This eliminates human errors and ensures data consistency.

Parity Tests: Protection Against Logic Discrepancies

Even with identical data, language differences can cause behavioral variances: Math.ceil() in TypeScript and num.ceil() in Dart may differ on edge cases. To prevent this, a parity testing system was implemented:

  • A set of fixtures—dozens of input combinations for each of the 61 calculators
  • The TypeScript engine runs through all fixtures, saving results to a reference file
  • The same file is used in the Flutter repo to compare against Dart engine results
  • Any discrepancy—even in the last decimal place—fails the CI

Over six months, these tests caught three real discrepancies that would have gone unnoticed without automated checks.

Google AdInline article slot

Three Accuracy Modes Instead of "Plus 10%"

Traditional calculators often use a universal safety factor (e.g., +10%). This shortcut introduces inaccuracies: tiles need trim allowance, plaster needs base unevenness compensation, and adhesive needs surface absorption.

Instead, three calculation modes were implemented:

  • Basic—standards-based calculation without margins, per GOST/SP
  • Realistic (default)—accounts for typical apartment renovation conditions
  • Professional—maximum margins for complex projects and shortage guarantees

Coefficients for each mode are also stored in JSON specs, allowing changes without rebuilding apps.

Key rule: for identical inputs, results must satisfy Basic ≤ Realistic ≤ Professional. A dedicated CI test verifies this for all calculators.

Validators as a Second Line of Defense

Beyond unit tests and parity checks, CI runs three specialized validators:

  • Logical validator: ensures purchase quantity ≥ net quantity, results don't zero out on valid inputs, labels use proper metric units (“m²”, not “sq m”)
  • Packaging validator: confirms realistic material packaging (adhesive in 5/10/15 L buckets, tiles in packs, not individually)
  • Units validator: catches metric system mix-ups (e.g., adding millimeters to meters without conversion)

These scripts act like a "security checkpoint" at the system gate—they don't replace unit tests but prevent silly yet critical ecosystem-level errors.

Key Takeaways

  • Single source of truth via JSON specs eliminates parameter duplication across runtimes.
  • Code generation ensures data sync without manual intervention.
  • Parity tests guarantee identical results in Dart and TypeScript.
  • Three accuracy modes replace simplistic "+10%" and give users control over margins.
  • Specialized validators in CI protect against systemic errors unit tests might miss.

— Editorial Team

Advertisement 728x90

Read Next