The problem
The platform serves municipalities and engineering consultancies. Their inspection reports, payment certificates and progress summaries are contractual documents, and each organization has its own format. The old approach was one generator class per client report. Every new customer meant new code, new bugs and one more thing to maintain forever.
I had written dozens of those generators myself. That is how I knew which parts were always the same.
What I built
- Templates carry the structure. A Word or Excel file, authored like a normal document, holds the layout and a set of placeholder patterns.
- A small template language of fifteen placeholder patterns, resolved against live data in a fixed, repeatable order, including repeating rows and tables that grow with the data.
- An adapter layer puts Word and Excel behind one interface, so the same logic fills either format, with PDF export on top.
- Read-only data models replaced a legacy path that wrote to the database while building a report. Generating a document can no longer change anything.
- An image pipeline that respects DPI, EXIF orientation and modern phone formats, so site photos print at the size and angle they were taken.
Migrating a live system
There was no moment when everything switched over. The engine grew beside the old path for two years. It started as one method, became a set of focused modules, and took over report by report. Only then was the old monolith deleted, nearly fifteen thousand lines in one change, with the callers already moved and verified.
A new report type is now a matter of authoring a template, not writing and maintaining another one-off codebase.
Proof, not adjectives
One customer’s weekly report had started timing out. A quick CPU check showed the process was waiting on the database, not computing. The cause was a nested lookup that ran thousands of queries per report. With the lookups cached, the report went from over 16 minutes to 32 seconds. The output was diffed cell by cell against the old version: 21,241 cells, zero differences. Every other caller of the changed code was audited, and one hidden bug found along the way was fixed too.