Virtual Blood Bank has a lot of operational data, but the mobile client should not have to assemble it from dozens of raw endpoints. A healthcare worker needs to know what is in stock, what is low, what is expiring soon, which requests are incoming, and which outgoing requests are still active.

The backend exposes that as a dashboard endpoint. FacilityDashboardService gathers inventory summaries, low-stock alerts, expiring-soon blood types, incoming pending request IDs, and active outgoing request IDs for the user’s facility.

This is a good use of Django as an application backend rather than a thin database proxy. The frontend asks for the dashboard. The backend decides what the dashboard means.

ORM summaries as workflow support

Inventory summaries use values("blood_type").annotate(total_units=Count("id")). District inventory groups by facility and blood type, scoped to active facilities in the same woreda. Inventory detail adds computed serializer fields like is_expiring_soon and is_expired.

These are small ORM features, but they carry real product value. A clinician does not need every unit row to know whether a nearby facility has compatible stock. A supply user does need unit-level expiry information when managing inventory. The API supports both levels.

The tradeoff is freshness versus cost. The dashboard computes current data when requested. That is simple and accurate for the current scale. If facilities, units, or request volume grew large, some summaries might need materialized tables or cached counters. I would not add those until the queries proved too expensive.

Notifications without starting with Celery

The project persists NotificationEvent rows when domain transitions happen. A delivery service later turns events into per-user NotificationRecord rows and sends through configured adapters. The default adapter logs to stdout. The FCM adapter becomes active when Firebase credentials and device tokens exist.

That design separates event creation from delivery. The lifecycle transition does not need to know whether the current environment can send push notifications. It records the domain event, and delivery infrastructure handles the channel.

For the MVP, this avoids a broker. No Celery, no Redis, no paid SMS dependency. The tradeoff is that delivery is simpler and less durable than a full queue with retries, dead-letter handling, and delivery attempt records. For a production healthcare deployment, I would expect stronger delivery guarantees. For this backend milestone, persisted events plus a scheduled dispatcher gave a clean path without infrastructure weight.

Audit as a separate concern

The transition service also writes audit records through a small record_audit helper. Audit failures are swallowed deliberately so an audit write problem does not block the clinical action. That is a tradeoff: the action succeeds even if the audit trail misses an entry.

Whether that is acceptable depends on policy. The current implementation favors operational continuity. A stricter compliance environment might require the opposite: if audit cannot be written, the action should fail. The important thing is that the choice is visible in one helper, not scattered through views.

What this project taught me

The biggest lesson from the dashboard and notification parts is that “read models” matter. The normalized database stores units, requests, users, facilities, events, and audit logs. The API still needs to shape those records into useful operational views.

Django’s ORM made those views straightforward enough to keep in the app instead of creating a separate reporting service. DRF serializers gave a stable response shape. Domain services kept the meaning out of the views.

Virtual Blood Bank made me think more carefully about backend responsibility. A good backend does not just expose tables. It protects workflows, summarizes operational state, emits events, and leaves an audit trail that future maintainers can reason about.