← Work

A multi-tenant attendance platform across industrial plants

Bridging biometric hardware, a third-party HR system and a mobile app as a modular monolith, on purpose

Role
Systems Analyst & Lead Developer
Context
Agro-industrial holding · multiple plants nationwide
Period
2022–2023
Outcome
Production across multiple companies, thousands of employees
Stack
.NET · Entity Framework · Angular · Android · Java · Kotlin · biometric terminals · C4 model

Context

An agro-industrial holding operating several companies and industrial plants across the country needed employees to mark attendance and access HR information from their phones. Until then, attendance meant a biometric terminal at the plant gate, and checking whether a punch had actually registered meant waiting for HR’s monthly report.

I designed and led the implementation of the platform that connects those three worlds: biometric hardware on the plant floor, a third-party HR system, and a mobile app in the employee’s hand.

Problem

Build an attendance and HR self-service platform that:

  • serves multiple client companies within the holding, with strict data isolation between them;
  • integrates with biometric terminals already installed across plants;
  • integrates with the third-party HR system that is the system of record for employee data;
  • synchronises between mobile clients and backend in near real time;
  • is architected so it can eventually be sold as SaaS to companies outside the holding.

Constraints

  • The biometric terminals were already deployed and could not be replaced. Those models only shipped an integration for .NET Framework 4.7.2.
  • The HR system was third-party and could not be modified, only integrated with.
  • Tenant isolation was a contractual requirement from the client, not a technical preference.
  • A team of four, single operations capability. Whatever we built, we had to maintain.
System context: employees and HR administrators, the mobile app, the platform, the biometric terminals and the third-party HR system.
System context: employees and HR administrators, the mobile app, the platform, the biometric terminals and the third-party HR system.

Approach

Modular monolith, with microservice-shaped boundaries

The obvious 2022 answer was microservices. I chose a modular monolith instead, and documented the module boundaries with C4 diagrams as though they were services, so that extraction later would be a deployment change rather than a redesign.

Reasoning: a small team, a single deployment target, and fewer than 15 tenants for the first launch. Microservices would have caused us more problems than benefits. To name one: independent scaling we did not need, at the cost of operational complexity we could not staff with the people we had. The module boundaries were the valuable part; distributing them was not.

The modules: organisational data, identity and access, attendance, and internal communications. Each with its own domain, its own persistence access, and an explicit contract to the others. A vertical-slice approach, ambitious for a technology that tempts you toward horizontal slices and solution-wide clean architecture. Here I pushed clean architecture down into each module. If you ask me, it was too ambitious, and that is why it took us so long. Over time I learned to be more pragmatic.

Container view of the platform: mobile app, admin panel, API modules and the shared tenant database.
Container view of the platform: mobile app, admin panel, API modules and the shared tenant database.
Component view of the attendance module, including the terminal integration path.
Component view of the attendance module, including the terminal integration path.

A shared tenant database, with a dedicated option designed but never built

Tenant data isolation was implemented as a single tenant-shared database, separate from the system database (configuration and cross-tenant data), with every row scoped by tenant id and the connection resolved at request time. The design also included a path for a tenant to opt into its own dedicated database instead of the shared one, for a client whose contract demanded stronger isolation. That path was never built: all 14 tenants at handover ran on the shared database.

The trade-off: a shared database is always cheaper to operate: one migration target, one thing to patch and monitor. All fine, but at the cost of a weaker isolation guarantee than physical separation gives. It was a reasonable bet, since every client was satisfied by it anyway (they were the holding’s own companies, so there was not much of a problem there). What was not free was designing and implementing the dedicated-database escape hatch in the data-access layer for a requirement no client ever used. A high price to pay for building something that was not going to be used any time soon, purely to save future work that was uncertain anyway.

Integration in two directions

The biometric terminals were first integrated with the HR system directly, and then brought into the platform’s own APIs, a staged approach that let attendance keep working through the transition rather than requiring a cutover at every plant simultaneously.

On the other side, an integration layer sits between the proprietary mobile backend and the third-party HR system, so that the HR vendor remains the system of record while the platform owns the employee-facing experience.

Result

  • 14 tenants in production across the holding at handover.
  • Remote attendance marking and HR self-service for thousands of employees.
  • ~30% reduction in HR administrative workload, queries that previously required an office visit became self-service.
  • Architecture positioned for a future SaaS offering: although the shared-database model would need the dedicated-per-tenant path actually built before that story holds up outside the holding.

What I would do differently

Don’t design an escape hatch you don’t validate. The plan included a path for a tenant to opt into a dedicated database instead of the shared one, in case a client’s contract demanded stronger isolation. No client ever exercised it, so it sat in the data-access layer as complexity nobody used. I would ship the shared model alone and add the dedicated path the day a real contract required it, not before.

Check the isolation requirement per client, not once for all of them. I read “tenant isolation is contractual” as a blanket constraint and designed for the strictest case across the board. In practice the shared database satisfied every one of the 14 tenants at handover. The requirement was real, but I should have verified it against each client’s actual contract language instead of assuming the strongest interpretation applied everywhere.

Document for the operators, not just the architects. The C4 diagrams were good for explaining the design and useless for running the system at 3am. A platform across multiple physical plants needs runbooks as much as it needs architecture diagrams.

  • multi-tenancy
  • systems-integration
  • architecture-documentation
  • modular-monolith