Software Development

The Delta Health Information and Appointment Booking System: A Triumph Over Technical Realities

Seven months of dedicated development, a hundred pages of meticulously crafted documentation, and a live system serving rural clinics in Delta State, Nigeria, culminated in a project that tested the very foundations of its creator’s technical understanding. The Delta Health Information and Appointment Booking System, initially conceived as a final year project at the University of Port Harcourt, evolved into a critical tool addressing significant healthcare access challenges in the region, encountering and overcoming three profoundly specific and difficult bugs along the way.

This system was not an academic exercise confined to a controlled environment. It was designed to serve real clinics, supporting patients who often travel considerable distances to receive medical attention, only to be met with full waiting rooms, absent practitioners, or uncommunicated appointment cancellations. These users are not hypothetical; their access to essential healthcare is directly impacted by the reliability of the software. This reality presented a different kind of pressure than achieving a mere academic grade.

The project’s genesis involved a rigorous vetting process. Before any code was written, the entire concept had to be presented and defended to university supervisors, the Head of Department, the Faculty Dean, and an external examiner. The system’s viability was scrutinized not only for its technical merit but also for its practical applicability in addressing a genuine problem faced by the people of Delta State. This intense scrutiny, requiring the presenter to anticipate questions from individuals with decades of experience, served as a crucible, sharpening the developer’s resolve.

The technical architecture of the system was robust, employing a modern stack including React.js for the frontend, Node.js and PHP with Laravel for the backend, and MySQL for the database. Deployment was managed on AWS, with Docker facilitating containerization. Crucially, the system was designed to accommodate the diverse technological landscape of rural Delta State. Communication channels included SMS gateways, the WhatsApp Business API, and USSD, recognizing that not all patients possess smartphones. The system was engineered to function effectively on 2G connections, on entry-level Android phones with limited RAM, and in areas where electricity is inconsistent and internet access is an intermittent luxury rather than a guarantee. It was within this challenging operational context that three particularly recalcitrant bugs emerged, each exposing a critical gap between theoretical knowledge and practical application.

Bug 1: The Clinic That Appeared in the Ocean

The location-based functionality of the system was a point of significant pride. It allowed patients to book appointments by surfacing nearby clinics based on their GPS coordinates, a vital feature for individuals in rural areas who might not know the precise names of the nearest healthcare facilities. The system aimed to connect them with the most accessible option.

Clinics were represented in the database with latitude and longitude stored in DECIMAL(10,7) fields. During development, with meticulously entered data, this feature performed flawlessly. However, the real-world test, involving data manually entered by clinic administrators from printed forms, revealed a critical flaw. A clinic in Delta State, Nigeria, began appearing on maps off the coast of Ghana.

This was not a metaphorical misplacement. The issue stemmed from the human element of data entry. Clinic administrators, transcribing coordinates from handwritten forms, introduced transcription errors. Latitude and longitude values were sometimes swapped, digits were missing or duplicated due to misreading handwritten numbers. While MySQL accepted these values as valid decimals, they pointed to entirely incorrect geographical locations. Six degrees of longitude and five degrees of latitude, while mathematically sound, were placing clinics hundreds of miles from their actual positions.

The resolution involved a two-pronged approach. First, a server-side validation layer was implemented in the Node.js API. This layer checked every incoming coordinate pair against Nigeria’s established geographic bounding box before data was written to the database. Coordinates falling outside this predefined area were flagged for review and prevented from being saved. Second, a one-time audit script was developed to systematically review all existing records, identify outliers, and flag them for manual correction. This process uncovered nine instances of mislocated clinics: six with swapped coordinates and three with numerical transcription errors.

The technical implementation of the validation logic involved defining the geographical boundaries of Nigeria.

// Validate coordinates fall within Nigeria's bounding box
function isValidNigerianCoordinate(lat, lng) 
  const NIGERIA_BOUNDS = 
    minLat: 4.0,
    maxLat: 14.0,
    minLng: 2.7,
    maxLng: 15.0,
  ;
  return (
    lat >= NIGERIA_BOUNDS.minLat &&
    lat <= NIGERIA_BOUNDS.maxLat &&
    lng >= NIGERIA_BOUNDS.minLng &&
    lng <= NIGERIA_BOUNDS.maxLng
  );


if (!isValidNigerianCoordinate(lat, lng)) 
  return res.status(400).json(
    error: 'Coordinates fall outside expected geographic boundary. Please verify and resubmit.',
  );

This bug underscored a crucial lesson: coordinate validation must extend beyond mere data type adherence to encompass geographical reality. A number can be mathematically valid yet factually incorrect, leading to significant real-world consequences.

Bug 2: The Notifications That Went Nowhere

The communication layer of the system was a significant concern from the outset. Ensuring patients received timely appointment confirmations and reminders through their preferred channels – SMS for basic phones, WhatsApp for smartphones, and USSD as a fallback – was paramount. The system included a notifications table to meticulously track every outgoing message, its type, content, status, and timestamp.

During integration testing, the system appeared to function correctly. API calls to the SMS gateway consistently returned 200 status codes, and the notifications table registered every message as sent. Confidence was high entering the field testing phase. However, feedback from a clinic administrator in the pilot program revealed a stark reality: none of the patients had received their reminders.

An investigation into the notifications table and API logs confirmed that every record indicated a sent status and every API call had returned 200. All technical indicators suggested the messages were being dispatched. Yet, they were not reaching their intended recipients.

The root cause of this discrepancy, uncovered after three days of intensive debugging, lay in a misunderstanding of the SMS gateway’s delivery model. The gateway operated on a two-stage delivery system. Upon receiving a batch of notifications from the system, it would acknowledge receipt with a 200 status code, confirming its acceptance. This response, however, did not guarantee actual delivery to the end-user. Actual delivery was an asynchronous process. The gateway would attempt delivery and, if a webhook URL had been registered, would subsequently call back to update the final delivery status. Crucially, this webhook had not been configured. As a result, messages entered the gateway’s queue, with some succeeding and others failing, but without any feedback mechanism, the system had no visibility into these outcomes. The sent status was being interpreted as delivered, a critical misinterpretation.

The corrective measures involved reconfiguring the notification sending process. Instead of assuming a 200 status meant delivery, the system was updated to reflect a more accurate initial state, such as queued. Furthermore, a webhook URL was registered with the SMS gateway to receive real-time delivery status updates. A dedicated webhook handler was implemented to process these incoming receipts, updating the notification status in the database to either delivered or failed. For failed deliveries, a retry mechanism was introduced to queue messages for subsequent attempts.

The revised code structure reflected this improved approach:

// What we had before (wrong)
const sendNotification = async (userId, message) => 
  const response = await smsGateway.send( to: phone, body: message );
  if (response.status === 200) 
    await db.notifications.update( status: 'sent' );
    // WRONG: this is gateway receipt, not delivery
  
;

// What we built after (correct)
const sendNotification = async (userId, message) => 
  const response = await smsGateway.send(
    to: phone,
    body: message,
    webhookUrl: `$process.env.BASE_URL/webhooks/delivery-receipt`,
  );
  await db.notifications.update( status: 'queued' ); // Honest status
;

// Webhook handler for actual delivery confirmation
app.post('/webhooks/delivery-receipt', async (req, res) => 
  const  messageId, status  = req.body;
  await db.notifications.update(
    messageId,
    status: status === 'delivered' ? 'delivered' : 'failed',
  );
  if (status === 'failed') 
    await retryQueue.add( messageId ); // Queue for retry
  
  res.sendStatus(200);
);

This adjustment dramatically improved confirmed delivery rates, climbing from a catastrophically low baseline to 94%. The remaining 6% represented unreachable phones or switched-off devices, a realistic metric for any rural telecommunications environment.

The key takeaway from this bug was the critical distinction between gateway acknowledgement and actual end-user delivery. The lesson: always read the complete documentation, implement webhooks for status updates, and never assume a 200 status code signifies a guaranteed outcome.

Bug 3: The Bug That Only Existed in the Real World

Perhaps the most vexing bug was one that stubbornly refused to manifest in the development environment. The system was designed with bandwidth limitations in mind, featuring offline caching for its health education hub and UI optimizations for slow connections. Network resilience was a core design principle.

However, the system had not accounted for the intricacies of database transactions when network connectivity faltered mid-process. Specifically, when a patient initiated a booking, the connection dropped, connectivity was restored, and the patient, assuming the initial attempt had failed, re-submitted the request. This scenario led to duplicate appointments appearing in the database: the same patient, the same clinic, and the same time slot booked twice.

The sequence of events was as follows: a patient initiates a booking, the client generates a request, the connection is lost before the request reaches the server. The client might display a loading indicator. Upon connection restoration, the patient, unaware the initial request was in transit or failed, re-submits. Both requests, upon eventual arrival at the server, are processed and recorded as separate transactions because the database logic lacked a mechanism to identify them as duplicates.

The solution to this persistent issue was the implementation of idempotency keys. An idempotency key is a unique identifier generated by the client for each transaction. This key is sent along with the request to the server.

On the client side, a unique key is generated for each booking attempt, incorporating elements like user ID, clinic ID, slot ID, and a timestamp to ensure uniqueness. This key is stored locally before transmission.

// Client side: generate a unique key per booking attempt
const initiateBooking = async (bookingData) => 
  const idempotencyKey = `booking_$userId_$clinicId_$slotId_$Date.now()`;

  // Store key locally before sending
  localStorage.setItem('pendingBookingKey', idempotencyKey);

  const response = await fetch('/api/appointments', 
    method: 'POST',
    headers: 
      'Content-Type': 'application/json',
      'Idempotency-Key': idempotencyKey,
    ,
    body: JSON.stringify(bookingData),
  );

  if (response.ok) 
    localStorage.removeItem('pendingBookingKey');
  
;

On the server side, before processing a new appointment request, the system checks for an existing record associated with the provided idempotency key. If a matching key is found, indicating a previously processed request, the server returns the original response without creating a duplicate entry. If no matching key exists, the transaction proceeds, and the idempotency key is stored alongside the new appointment record.

// Server side: check key before processing
app.post('/api/appointments', async (req, res) => 
  const idempotencyKey = req.headers['idempotency-key'];

  const existing = await db.appointments.findByKey(idempotencyKey);
  if (existing) 
    return res.status(200).json(existing); // Return original response
  

  const appointment = await db.appointments.create(
    ...req.body,
    idempotencyKey,
  );

  res.status(201).json(appointment);
);

In addition to server-side logic, client-side enhancements included a state machine to accurately reflect the booking status in the UI – pending, confirmed, or failed. Local caching was also implemented to ensure that bookings initiated during network disruptions would automatically retry once connectivity was restored.

The lesson here is profound: systems must be tested under the conditions they are intended to operate. A stable development environment can mask entire categories of bugs that only surface in environments with unreliable infrastructure. Idempotency is not a mere convenience; it is fundamental to ensuring data integrity and preventing critical errors in distributed systems.

The Weight of Seven Months: Lessons Learned and a Lasting Impact

It is important to emphasize that these bugs were not products of esoteric coding practices or unthinkably complex algorithms. Their difficulty stemmed from the necessity to move beyond preconceived assumptions and confront the nuanced reality of how users interact with a system in a specific context. Administrators made transcription errors due to a lack of input validation. Patients re-submitted requests because the UI did not clearly indicate successful submission. The gateway’s 200 response was perceived as sufficient confirmation, a reasonable assumption in many scenarios, but not in this one.

The development process also prioritized comprehensive documentation. Building a system intended to outlast the developer’s direct involvement necessitated documenting not just the code’s functionality but also the rationale behind every design decision and every fix. This commitment to detailed reasoning is now a carried-forward practice in every subsequent project.

The Delta Health Information and Appointment Booking System is now live at deltahealth.usestudybuddy.org. It serves patients in Delta State, providing them with improved access to healthcare. Clinics that previously lacked any digital infrastructure now possess a real-time overview of their appointment flow and a means to proactively inform patients, preventing wasted journeys.

The pride in the system’s current functionality is significant, but it is matched by an even greater pride in the numerous times the project nearly faltered, only to be salvaged through persistent problem-solving and adaptation. This journey, marked by technical challenges and human-centric solutions, underscores the vital importance of understanding the real-world context in which software is deployed.

Tobore, the full-stack developer behind this impactful project, is a recognized author on DEV for React writing, focusing on tools that genuinely transform developer workflows. His work on the Delta State Health Information and Appointment Booking System exemplifies the power of applied technology to address critical societal needs, even in the face of formidable technical hurdles.

Related Articles

Leave a Reply

Your email address will not be published. Required fields are marked *

Back to top button