Home
All articles
GovTech.NETArchitecture

Modernizing Government Software Without Breaking It

June 18, 20267 min read

Government software has a reputation, and mostly it's deserved: dense forms, decades of accumulated rules, and a very low tolerance for downtime. At Catalis I work on Tax & CAMA — a suite that helps local governments run billing, collections, and property assessment across all 50 US states. When a system moves millions in annual tax revenue, "let's rewrite it" is rarely the right answer.

The interesting engineering problem isn't writing new code. It's replacing old code while the plane stays in the air.

Modernize the edges first

The safest place to start is the surface. Legacy back-ends often outlive three generations of front-ends, so wrapping a stable API in a modern React interface delivers visible value with almost no risk to the money-moving core. Users feel the upgrade immediately; the ledger logic doesn't change at all.

  • Put a clean API boundary in front of the legacy data layer, even if it's just a thin façade at first.
  • Rebuild one workflow end-to-end in React before touching the next — a vertical slice, not a horizontal layer.
  • Keep the old screen reachable until the new one has run in production long enough to trust.

Treat data migrations as first-class features

Schema changes in this world are not chores you sneak into a Friday deploy. They get their own tickets, their own tests, and their own rollback plan. A reversible migration with a verified backup is worth more than any clever refactor.

In government software, boring deployments are the whole goal. Excitement means something went wrong.

Build reusable cores, not one-off screens

Across 50 states you see the same shapes over and over: a searchable roll, an assessment detail, a workflow with approvals. Investing early in a small set of reusable core components paid for itself many times — new modules dropped in weeks instead of months, and the UI stayed consistent without a design police force.

csharp
// A projection-only read keeps the assessment grid fast — we never
// materialize the full entity graph just to render a table row.
var rows = await db.Assessments
    .Where(a => a.CountyId == countyId && a.TaxYear == year)
    .Select(a => new AssessmentRow(a.Id, a.ParcelNumber, a.Owner, a.MarketValue))
    .AsNoTracking()
    .ToListAsync(ct);

Modernization isn't a heroic rewrite. It's a hundred small, reversible decisions that each leave the system a little better than you found it — and never leave it worse.