Health 1000 — Hospital Management System
A complete hospital management system, sold to a hospital and in active development: a .NET 10 backend that runs patients, wards, clinics, pharmacy, labs, inventory, and accounting, and the Flutter desktop app that ten clinical and finance roles work in all day.
- My role
- Full-Stack Engineer (backend owner, lead Flutter developer)
- Period
- May 2026 – Present
- Related experience
- Evolvo Tech
Health 1000 is a hospital management system I build at Evolvo Tech. It is sold to a hospital and in active development. I designed and built the ASP.NET Core (.NET 10) backend, and I am the lead developer of the Flutter Windows app the staff use. The product is Arabic-first and fully bilingual.
At a glance
- Backend: ~66k lines of C# in a six-project Clean Architecture solution — 94 domain entities, 285 REST endpoints, 42 EF Core migrations, and ~90 PostgreSQL views behind the read side.
- Client: ~133k lines of Dart — 27 feature modules, 35 BLoCs, and 2,360 localization keys in both Arabic and English.
- Tests: ~12.5k lines. C# unit tests, integration tests against real PostgreSQL and MinIO (Testcontainers), and Dart tests including an Arabic/English parity check.
- Load-tested: 33 concurrent staff accounts for four minutes — 60,509 requests at ~252 req/s, p95 322 ms, zero server errors.
What the hospital runs on it
Ten staff roles, each with its own workspace: reception, doctors, nurses, head nurses, lab and imaging technicians, pharmacists, accounting, inventory managers, and management.
- Patients and visits. Patient files, admissions, rooms and beds (occupied, reserved, cleaning, maintenance), and outpatient clinic queues with appointments.
- Clinical work. The doctor workspace (diagnoses, notes, orders, referrals, escalations), nursing (medication schedules, care plans, shift handoffs, tasks), and lab and imaging boards where due times come from each test’s turnaround.
- Pharmacy and stock. Dispensing, drug stock, the warehouse, and medical supplies that nurses draw and return.
- Money. Patient accounts, insurance, doctor fees and settlements, cashboxes, and expenses.
- Operations. Shifts, attendance, and on-call rosters. Notifications escalate when nobody acknowledges them in time. Scheduled PDF reports and a live manager dashboard.
Backend architecture
- Clean Architecture + CQRS (MediatR), organized in vertical slices by feature.
Controllers are one line: dispatch the command, map the
Result<T>. - The pipeline order is deliberate: exception logging → timing → validation → idempotency → caching. Bad input is rejected before it can reserve an idempotency key, and a cache hit skips only the handler.
- No exceptions for business rules. Domain and application code return
Result<T>with a localization key, and one translator turns it into a localized RFC 7807 problem response. - Bilingual data uses a
LocalizedTextvalue object stored as JSONB. The API is versioned and uses snake_case on the wire.
Money that adds up
Accounting was the hardest part to get right, and the part I’m most proud of.
- Open accounts. A patient’s invoice opens at registration. Each clinical event — a lab
result, a dispensed drug, a used supply, another night in a bed — adds its charge
automatically. Each charge is keyed to its clinical source with a partial unique index and
ON CONFLICT DO NOTHING, so the sync can run on every event and never bills twice. - Nothing silently billed at zero. Work on a service with no price is counted and shown on the account, not quietly dropped.
- Safe under concurrency. Money paths run in
Serializabletransactions with hand-writtenSELECT … FOR UPDATElocks on invoices and cashboxes (EF Core has no pessimistic locking). Two cashiers taking payment on the same account cannot overwrite each other. - One place decides totals. A single SQL recalculation splits every invoice between patient and insurer. It keeps the insurer snapshot taken when the invoice opened and never reopens a refunded, written-off, or cancelled invoice. Its rounding matches the C# domain to the piastre.
- Collision-free numbering. Invoice, receipt, and settlement numbers come from
PostgreSQL sequences. The sequence name is bound as a
regclassparameter, never pasted into SQL. - Doctor-fee engine. Fee rules (fixed, percentage, or both, with priority) automatically split each line between the doctor and the hospital. Settlements subtract a doctor’s advances and are created and paid in one transaction. The payout cashbox comes from the signed-in cashier, never from the request.
- Close, collect, reopen. Closing and collecting is one atomic, retry-safe step. Reopening an account for a late charge is manager-only and audited.
- I also split a 2,816-line accounting god class into focused stores around a small shared kernel of locking, recalculation, and numbering.
Inventory and pharmacy
- Lot-level stock with FEFO. Stock leaves soonest-expiry first. Every movement writes one row per lot, so the audit trail shows exactly which physical goods left. A single writer keeps each item’s total equal to the sum of its lots.
- Per-line dispensing. A patient prescribed five drugs gets the four in stock, with substitutions recorded. The whole prescription no longer fails over one missing item. “On order” is stored, not guessed, and a nightly job archives settled prescriptions.
Reliability and performance
- Transactional outbox. Domain events are saved in the same transaction as the change.
The dispatcher claims them with
FOR UPDATE SKIP LOCKED, which fixed a race that published the same event twice (a test reproduces it). Failures are retried and then dead-lettered. - Idempotency keys. Commands with an
Idempotency-Keyreserve it against a unique database constraint, so exactly one concurrent request wins. Retries get the stored response back. - Real-time without stalling the database. An EF Core interceptor maps each saved change to the dashboard sections it affects. SignalR hints go through a bounded, drop-oldest queue, so a slow WebSocket client can never hold a database transaction open.
- Built to shed load. Rate limits are tiered (auth, read, write) and partitioned per user. Over-limit requests are rejected immediately instead of queued. Request timeouts and health checks cover PostgreSQL, MinIO, and the outbox, and probes are never throttled.
- A connection-pool bug, found and fixed. Registering enum mappings per scope made Npgsql
build a new connection pool on every request, which exhausted PostgreSQL in about 30
requests. One shared
NpgsqlDataSourcefixed it. - Measured, not assumed. I wrote a load-test harness with one real account per virtual user, so it measures the server rather than the rate limiter. The first run exposed 30-second report timeouts and 500 errors. After the fixes, the four-minute run held p50 104 ms, p95 322 ms, and p99 525 ms with no server errors.
One endpoint for the read side
About 90 PostgreSQL views are served through one endpoint, /read-models/{relation}. It
stays safe and fast:
- Relation names are checked against an allowlist.
- Column names come from the response DTO, and filter values are always parameters.
- Each page and its total come from one query using
count(*) OVER (). - Page sizes are capped.
- Reference data is output-cached, and queries use HybridCache.
Security and access
- Authentication. ASP.NET Core Identity with JWT access tokens and rotating refresh tokens.
- Per-record access, not just roles. A doctor can open a visit only as its attending doctor, a referral target, an escalation taker, or a care-team member.
- Password migration. Users carried over from the earlier backend keep their passwords: old bcrypt hashes are verified and upgraded to Identity v3 at next sign-in.
- Files live in MinIO with per-bucket access policies. They are served through presigned URLs or streamed by the API.
The Flutter desktop client
- Windows-first Flutter Desktop, right-to-left Arabic by default. Each feature is split into data, domain, facade, and presentation layers, using BLoC, GetIt/Injectable, go_router, freezed, and reactive_forms.
- Request cancellation through Dart Zones. Switching sidebar tabs used to leave the old
screen’s requests on the wire, so the new tab waited behind them. A
RequestScopecarried in the DartZonenow cancels them across ~300 call sites in 28 data sources — without passing a cancel token through every layer. A pooled HTTP adapter caps sockets per host. - Real-time lifecycle. One coordinator connects and disconnects SignalR based on sign-in state and app lifecycle, with a grace window for short app switches. Dashboard sections refresh when the server says they changed.
- One design system. One shared table system. One diagnostics board replaced two copied features (lab and radiology). One patient-file layout works for every role. The UI only offers actions the API allows for that role.
- Desktop polish. System tray, launch at startup, a remembered window state, desktop notifications, and printable PDF invoices and settlements. Light, dark, and system themes, larger text, and an Inno Setup installer. Each install is checked against the server.
From Supabase to a custom .NET backend
The system started on Supabase. As it grew into accounting, concurrent payments, scheduled jobs, and audit trails, I rebuilt the backend in .NET 10 and kept the PostgreSQL data model:
- Views and business triggers were rebuilt as migrations.
- Parity tests check that every column survived the move.
- Existing password hashes kept working.