Case study 02 · Modernisation

Inheriting a reporting service and making it last

Context
Business document generation for an enterprise client
My role
Took over as the main maintainer in 2025, then upgraded, extended and hardened it.
When
2025 – now
Stack
.NET · Playwright · Handlebars · SQL Server · Redis · MassTransit · Azure Service Bus

The problem

The service produces the documents the business runs on: invoices, delivery and gate documents, purchase offers and inspection reports. It had been in production for years on an older .NET version and a single commercial reporting engine, with no automated tests. People depended on its output every day, so a rewrite was never on the table.

What I did

  • Upgraded the runtime twice and the reporting engine once. Moved to central package management and a build pipeline. Replaced raw HTTP calls to around a dozen downstream services with typed clients that support cancellation.
  • Built the inspection report end to end. It pulls evaluation data, photos, test and diagnostic results, and a plain-language summary written by an internal AI service.
  • Introduced a second rendering engine alongside the existing one: HTML templates rendered to PDF in headless Chromium, with templates editable at runtime. It touches none of the old engine’s code. Colleagues have since moved other documents onto it.
  • Diagnosed font and template crashes that only appeared in Linux containers.
  • Hardened rendering: an isolated browser context per document, a resource allow-list, a concurrency limit, and automatic browser relaunch after a crash.

Decisions and tradeoffs

  • Additive, not a rewrite. Both engines run side by side. Documents move across one at a time, and nothing that works today has to change on the same day as everything else.
  • Golden tests as the safety net. Every report has snapshot tests that lock its data model, its HTML and its PDF: page text plus an image of each page. A test fails if a report has no golden case at all. Baselines are kept per operating system, because fonts render differently.
  • Boring mapping over clever mapping. I replaced automatic object mapping with explicit code, so a renamed field breaks the build instead of a PDF.

Where it landed

The test suite went from nothing to around 117 tests, most of them golden cases. The new engine is how new documents get built now.

api requestbus eventdata assemblytyped clients ×12routerexisting enginecommercial · in placehtml enginehandlebars → chromiumpdfdocument storefiled · event publishedgolden testsdata model · html · pdf text · png per page① both engines run side by side② templates editable at runtime③ a report with no golden case fails the build④ isolated browser per render, relaunch on crash
my partexisting
Original, simplified diagram. Not the real system.

Switch on “Show working” for the annotations.