The operating system for a fifteen-clinic medical group

Replacing a scatter of disconnected tools with one platform: public site, booking, patient records, lab logistics, multi-warehouse inventory, fiscal accounting, HR and advertising reporting - behind a single admin with clinic- and region-level isolation across nine staff roles.

13 clinics on one platform
9 staff roles, each with its own view
8 systems replaced by one
0 outages that can block a booking
MY ROLE
Architecture and all of the code. I set the data model, wrote the backend and the front end, built the admin and run the infrastructure it sits on.
STACK
Django 5.2, PostgreSQL, Redis, Celery, server-rendered front end, Nginx on hardened Ubuntu.
HARDEST PART
Not the features. Making fifteen sites share one system without any of them being able to see another's patients.

The situation

A medical group with fifteen clinics was running on eight things that did not know about each other: a website, a separate booking widget, a spreadsheet for stock, a second spreadsheet for the laboratory, an accounting package, a time-and-attendance app, an advertising dashboard and a shared inbox. Every number that mattered existed in at least two places, and where they disagreed nobody could say which was right.

That is not a technology problem. It is a business problem that shows up as a technology problem, and it is the shape of work I am best at.

What I built

One Django platform, serving all fifteen sites: the public website, appointment booking, patient records, the CRM, laboratory logistics, inventory across multiple warehouses, fiscal accounting, HR, and reporting on advertising spend. All of it behind one administration interface with clinic- and region-level isolation across nine staff roles, so a receptionist in one city cannot see another city's patients and does not have to think about why.

The features are the unremarkable part. Three of the systems underneath them are worth describing, because they are the difference between software that works on the day it launches and software that is still trustworthy two years later.

A consent flow that would survive a court

Medical consent under GDPR is not a checkbox with a timestamp. What you have to be able to produce, later, is the exact document a specific person agreed to at a specific moment - not your current version of it.

So the flow generates the document, shows it on screen, confirms identity with a code sent by SMS, and stores the rendered PDF together with the wording, the language it was displayed in, the address it was signed from and a hash chain linking it to the record. Editing the template afterwards changes what the next patient sees and nothing about what previous ones agreed to.

The rule underneath: consent is to a sentence, not to a boolean. If your system stores true, it has stored the wrong thing.

Inventory as a ledger, not a number

Most stock systems keep a quantity column that code writes to. That column is wrong eventually - a race, a retry, a mis-click, a partially applied transfer - and once it is wrong there is no way to find out why, because the evidence was overwritten by the answer.

Here, quantity is not stored. Every receipt, transfer, consumption and correction is an immutable signed entry, and the balance of any warehouse is the sum of the entries that touch it. Corrections append the inverse rather than editing history. That makes reconciliation a property of the design rather than a monthly task, and it means the answer to "why do we have eleven of these?" is always available and always complete.

There is a working version of this idea further down the page.

A calendar that cannot double-book

Fifteen clinics' worth of appointments overlap constantly, and the front desk needs to see a whole day at once without two bookings ever occupying the same slot. This is the interval-scheduling problem: sort by start time, then put each appointment in the first lane whose previous appointment has already finished.

What makes it worth doing properly rather than approximately is that the greedy version is provably optimal - the number of lanes it produces is exactly the largest number of appointments that overlap at any single instant, which is a hard lower bound no algorithm can beat. You can watch that hold on the demo below.

Romanian reality

Invoicing, the cash-register journal, and drivers for the fiscal printers the law requires. Prosthetics tracked by courier between clinic and laboratory. Time and attendance with a small desktop client sending a heartbeat. None of this is interesting engineering and all of it is the difference between a system the clinics use and a system they work around.

The decision I would defend hardest

Every external dependency in the platform fails open. If the SMS provider is down, the advertising API is rate-limiting, or the invoicing service times out, the booking still completes and the work is queued and flagged for a human.

The alternative - refusing a booking because a third party is unavailable - means outsourcing your revenue to somebody else's uptime. The internal protections work the other way: rate limits, validation and the honeypot fail closed, because those have no third party to blame.

What I would do differently

I would build the reporting layer earlier. I designed the ledger and the scheduling correctly from the start and let reporting accrete, which meant the first three months of management questions were answered with queries I wrote by hand. Correct, but slow, and I should have seen it coming.

TRY IT · THE SCHEDULING ALGORITHM

The calendar packer, running here.

This is the interval-scheduling routine described above, in your browser. Add appointments and it keeps using the fewest possible collision-free lanes.

This is an interactive demo and needs JavaScript enabled.

TRY IT · THE INVENTORY LEDGER

Stock derived from signed movements.

The same ledger design, simplified to three locations. Nothing here stores a quantity - post a movement and watch the totals recompute from the log.

This is an interactive demo and needs JavaScript enabled.