Skip to content
Kumar Chandrachooda
.NET

What a Hand-Rolled Intranet App Taught Me

The honest retrospective - copy-paste as a pattern, zero tests, PII in telemetry and what employee-monitoring data demands, destructive error handling, and the handful of decisions that genuinely earned their keep.

By Kumar Chandrachooda 02 Jul 2025 7 min read
Fourteen parts of anatomy, weighed on one scale

Fourteen parts of anatomy, and now the coroner's summary. Part one opened this series on a platform I built at a mid-size IT services company — a hand-rolled attendance system that ran in production for years and did its job. This final part is the ledger for the whole thing: what earned its keep, what I would never do again, and the one class of obligation the platform never met that I think about more than any bug. An honest retrospective is not a confession and not a defence; it is an attempt to separate the decisions that were wrong from the decisions that were merely mine, made under a deadline, with the information I had.

What copy-paste became

The single most defining pattern in the codebase was duplication, and it is worth naming as a pattern rather than a series of accidents, because that is the lesson. The payroll-month arithmetic lived in four files. There were four near-identical dashboard controllers — Employee, HR, Manager, Team Lead — each a copy of the others with a role-shaped tweak. Six near-identical Excel-export branches. Three copies of the same reporting logic. The stack-trace-destroying catch (Exception ex) { throw ex; } appeared thirty times.

Each copy was individually rational. It is Friday, the HR dashboard needs to be slightly different from the Employee one, and copying the working controller and changing three lines is fifteen minutes, while factoring out the shared shape “properly” is an afternoon and a risk. Copy-paste is not laziness; it is a loan at a rate you cannot see, and the repayment comes due the day a rule changes. When the payroll month or the contractor-exclusion pattern needed adjusting, it needed adjusting in four places, and the fourth was always the one that got missed — so the four copies disagreed, and now the same metric meant different things depending on which screen you were on. Duplication does not cost you when you write it; it costs you when the world changes and only some of the copies hear about it.

Zero tests, and what that actually meant

There was no test project. Not a thin one — none. The CI pipeline ran dotnet test, which found no tests, passed instantly, and printed green. Every reader assumed the app was tested because the pipeline was green; the green meant only that a command with nothing to do had done it — the ceremony of verification with none of the substance.

What the absence cost is the honest part. Every one of the bugs in this series — the December cache expiry, the wrong-list guard in the report builder, the ten-byte export file, the string.Format-over-CSS crash — is a bug a single test would have caught, and none had one. More than that: the absence of tests is why the copy-paste pattern was safe to indulge. If changing the shared payroll-month helper had a test suite standing behind it, factoring the four copies into one would have been a refactor with a safety net. Without tests, the safe move was always to copy, because touching shared code was unfalsifiable roulette. No tests and rampant duplication are not two problems; they are one problem twice — the missing safety net is what makes copying feel safer than sharing.

The obligation we never met

Now the part I think about most, and the reason this retrospective is not just an engineering post-mortem. This was an attendance and employee-monitoring platform. It knew, for thousands of people, when they arrived, when they left, whether they worked from home, how long, on which shift, and it inferred and stored judgements about all of it that fed payroll. That is not ordinary application data. It is data about people's working lives, held by their employer, and it carries obligations the platform never met.

Three concrete failures, each an anonymised pattern from the estate:

  • PII into telemetry. A custom telemetry initialiser stamped every Application Insights item with a user identifier built from a person's name and employee id. So the monitoring system — a third-party service, with its own retention, its own access model, its own region — became a second, unaudited copy of who-did-what, indefinitely. Combined with the unconditional sensitive-data logging from part five, the platform's observability was a shadow database of employee behaviour that nobody governed as one.
  • Monitoring data with no retention policy. The system gap-filled attendance back to 2019 and kept everything. There was no retention rule, no “this data ages out”, no answer to “how long may we hold a record of when this specific person came to work in 2019?” — a question that data-protection regimes have very definite opinions about, and that the platform had never been asked.
  • Impersonation with no audit. The thirty-plus-entry impersonation list from part three let administrators act as other employees, and nothing distinguished an impersonated action from a real one in any log. On a system where actions affect pay, “who actually did this” had no reliable answer.

None of this was malice; all of it was omission. And that is precisely the point I want to leave with: employee-monitoring data raises obligations that do not announce themselves — no exception is thrown, no build fails, when you over-collect, over-retain, and under-audit a record of people's working lives. The engineering was under-tested; the ethics were untested in a deeper sense — nobody had asked the questions, so nobody had failed them visibly. If I built this today, the data-governance design would come before the first controller, not after the fact and never.

What genuinely earned its keep

A retrospective that is all indictment is dishonest, because the platform worked, for years, and several decisions were right:

  • Derived roles from the org chart (part two). “Manager means has-direct-reports” was self-maintaining through every re-org, and I would do it again exactly so.
  • Reports as data (part eight). Storing report definitions in tables and serving self-service BI was the right architecture; the injection flaw was in the execution, not the design.
  • Alternate keys for a natural identifier (part five). Relating on the real badge number via HasPrincipalKey was honest about where identity actually came from.
  • Managed identity for the web app (part thirteen). No credential to leak, because there was no credential. The target state, reached.
  • The one deliberate IDOR guard (part eleven). Somebody thought about a specific threat and wrote a specific check. That instinct is the whole game.

What I would do differently

Concise, because the list is long and the theme is short:

  1. Tests first, so sharing beats copying. Not for coverage's sake — because a safety net is what makes de-duplication safe, and de-duplication is the fix for almost everything else here.
  2. One authorisation path in every environment, faking identity rather than compiling out the gate.
  3. Delete-and-document. Every abandoned framework, ghost package, and vestigial schema becomes a one-line tombstone, not a permanent puzzle for the next reader.
  4. Errors fail loudly. No throw ex;, no ten-byte success file, no swallow-to-default. A caught exception either handles or enriches; otherwise it is noise that destroys the one thing you will want at 2 a.m.
  5. Govern the data before you collect it. Retention, minimisation, and audit designed in from the first schema — because monitoring data's obligations never throw, and the only way to meet them is to ask the questions no build will ask for you.

The last word

I am proud of this platform and clear-eyed about it, and I do not think those are in tension. It solved a genuinely hard problem — turning badge swipes and self-reported work-from-home into fair, auditable, payroll-grade attendance for thousands of people across three cities — and it solved it well enough to run for years. It also carries every mark of software built by a small team under deadline, learning as it shipped: the duplication, the missing tests, the swallowed errors, the ungoverned data. The value of writing all fifteen parts is that the gap between what the code did and what I believed it did is the most durable thing I own from the work. Owning that gap in the first person is the point.

This series was the web-facing half of the estate. Behind it ran three Azure Functions, a scheduled console job, and the SQL database they all coordinated through — a system with no message bus that used its own database as one. That is the companion series, The Database Is the Message Bus, and it starts where this one's background work disappears to.