Padmavathi Sanapala, CTO and Chief Architect, RatnaGlobalTech, LinkedIn profile
Two hospitals went live on Ratna HealthPlex within a few weeks of each other. One was a two-branch eye care centre that wanted thirteen modules on day one, package billing for cataract and refractive procedures, and receipts that looked exactly like the ones its patients had carried home for a decade. The other was a multi-specialty hospital that wanted OPD and pharmacy first, itemised billing, two users, and a one-week trial before signing anything.
The easy engineering answer to that pair is a fork. Copy the repository, change what the second hospital needs, deploy it separately. Everyone who has run a product company knows how that ends. Within a year there are two products, each with half the testing, and a third customer who needs a bit of both.
We did not fork. This post is about the architecture that lets one build serve both hospitals, and the forty that come after them, without a single if (tenant == ...) in the code.
Tenant is a request property, not a deployment
A tenant in HealthPlex is a legal entity: the hospital, the group, the agency that signed the contract. A facility is a physical site under that tenant. The eye care centre is one tenant with two facilities. A hospital chain acquiring sites across the country is one tenant with a growing list.
Every request carries this context. The API gateway validates the JWT, resolves the tenant, facility and roles from the identity provider, and stamps X-Tenant-Id and X-Facility-Id on every downstream hop. The sixteen-plus Java 21 Spring Boot services behind the gateway never look at the token again. They read the headers, and the Feign clients they use to call each other forward the headers automatically.
A request that arrives at a service without a tenant header is rejected. It is never defaulted to a “system” tenant, never guessed from the data, never allowed through because the caller was internal. That one rule has caught more bugs in review than any other.

Isolation is enforced where the data lives
Application code forgets. A new developer writes a query, tests it against a single-tenant dev database, and ships a report that leaks one hospital’s revenue to another. Every multi-tenant product that has had a data incident has had one of these.
So the isolation is not in the application. Every table carries tenant_id. PostgreSQL row-level security policies enforce it at the database, using a session variable the service sets from the request header before the first query. A Hibernate filter enforces it a second time inside the ORM. A query that forgets the tenant returns nothing, not everything.
The same rule extends outward. Identifiers live in a separate PII vault with per-tenant encryption keys held in a KMS; the clinical tables carry tokens, not names. Documents and images sit in object storage under a tenant prefix. Configuration lives in its own versioned store with tenant and facility overrides, and every change is audited.
Sixteen services each own their schema, migrated with Flyway. Tenant onboarding does not create a new database. It inserts a tenant row, runs the specialty pack, and the platform is ready for the first user.
The difference between two hospitals is always a configuration key
This is the rule that makes the rest work. If two hospitals need different behaviour, that difference is a key with a default value, never a branch in code.
Configuration resolves in five layers, in a fixed order:
- Platform defaults. Every key has a value on day one. Nothing is required to run.
- Specialty pack. Eye care, dental, behavioral health, occupational health, hospice. Each pack ships tested clinical templates, billing rules and module lists. The eye care pack turns billing to package mode and adds the optical module.
- Tenant override. The hospital’s own commercial and clinical decisions, signed off during onboarding. Revisit-free window of seven days. Doctor revenue share on. Receipt template version three.
- Facility override. A branch can differ from head office where it must, and only there. The Bareilly branch extends the revisit-free window to fourteen days and prints on a thermal printer.
- Effective configuration. Resolved at login, cached per session, versioned. Every read is traceable to the layer that set it.

The consumers of that effective configuration are everywhere: the UI module registry that decides what appears in the sidebar and for which roles, the billing rules engine, the print templates for receipts and discharge summaries, the notification templates in each language, the workflow definitions that decide which gates fire at registration and discharge, and the jurisdiction pack that loads HIPAA or DPDP rules for consent and retention.
A worked example. A receptionist at the multi-specialty hospital was confused by a negative refund figure on the front office overview screen. Pharmacy was fine with negatives; the receptionist was not. The fix was not a special case for that hospital. It was a platform rule, expressed as configuration: printed documents show the real signed value, because a receipt that says the hospital owes the patient money must say so, while overview screens never show a minus sign and instead show billed, refunds and net as three positive columns. The split is by artifact type, not by module or user, and every tenant inherited it.
One shared UI
The frontend is a single React 18 and TypeScript build. It does not know which hospital it is serving until the user logs in and the tenant configuration arrives.
From that configuration it assembles itself. The module registry decides which modules load. Capability flags decide which actions appear inside them. Theme tokens set the colours, the logo and the typography. Schema-driven forms render registration, intake and clinical templates from definitions rather than hand-coded screens, which is how the eye care centre gets an ophthalmology exam sheet and the dental clinic gets a tooth chart from the same codebase. Print templates come from the same store.
We looked hard at how other products solve this. OpenMRS 3 registers frontend modules dynamically. Medplum drives its forms from FHIR Questionnaires. Both confirmed the direction: the UI is a runtime composition of registered pieces, and the registry is data.
The field app for home health and hospice, built in React Native, reads the same configuration endpoint. A nurse’s visit form on a phone in Brooklyn and a receptionist’s screen in Ghaziabad are shaped by the same resolver.
Roles are scoped to the tenant, then the facility
RBAC in a multi-tenant clinical system has to answer two questions on every action: is this role allowed to do this, and is this user allowed to do it here. Roles are composed of permissions and scoped twice, to the tenant and to the facility. A doctor with admitting rights at the Delhi branch has none at Bareilly unless the tenant says so.
The scoping is real, not cosmetic. In the security assurance build we ship to every prospect, the auditor persona is read-only and the security engineer cannot accept a risk finding. Those are not UI hints. The API refuses.
What this costs and what it buys
Layered configuration is slower to build than a fork. The first specialty pack took longer than a copy of the repository would have. The configuration store, the resolver, the audit of configuration changes, the schema-driven forms: none of it ships a feature a customer asked for by name.
What it buys is one build, one deploy, one release note, and one test suite that runs against every tenant’s configuration before a release. When the behavioral health agency in Texas needed Medicaid unit authorizations tracked against every session, that became a specialty pack. The eye care centre got the same release with the pack turned off and never noticed.
A fork is a second product with half the testing. Layered configuration is one product with a thousand shapes. If you are evaluating a hospital platform, ask the vendor how their two most different customers run on the same release. If the answer involves a branch name, you have your answer.
CTO and ChiefArchitect, RatnaGlobalTech.