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.
my partexisting
Switch on “Show working” for the annotations.