It was the final year of my college, and I was left with 6 months to build my major project, which was a healthcare app
{(Patients), (doctors), (appointments), (prescriptions)} All data were connected to each other in a hierarchical relation.
I picked MongoDB because people around me were using it. Spent two months building on it, hit three problems I could not cleanly solve, and migrated to PostgreSQL before submission.
Why MongoDB made sense at the start
When requirements are changing every week, the last thing you want is to deal with database migrations. MongoDB lets you skip that. No schema upfront, just start inserting data and add fields as you go.
// Week 1
{ name: "Rahul", age: 24, phone: "9876543210" }
// Week 3, requirements changed
{ name: "Rahul", age: 24, phone: "9876543210", bloodGroup: "O+", doctorId: "abc123" }
That doctorId is a plain string pointing to a doctor document. No foreign key. No constraint. If that doctor gets deleted, this field sits there pointing at nothing, and MongoDB does not throw a single error.
That part I figured out much later.
Problem 1: Without a schema, your data becomes inconsistent without you noticing
A blood group is one of eight values. An age is a positive number. These are fixed, they do not change shape from patient to patient.
By week four I had three versions of the same field across my patient documents.
{ name: "Patient A", bloodGroup: "O+" }
{ name: "Patient B", blood_group: "B-" } // different key name
{ name: "Patient C" } // field just missing
All three valid. Zero errors. When I queried patients for blood group, I got back maybe 60% of the actual results because the rest were stored under a different key name.
I ended up writing code to normalize all three formats before querying. That is time spent fixing a problem that the database should have prevented in the first place.
Problem 2: No joins means you manually assemble related data in code
Every feature in a healthcare app touches multiple entities. Patient has appointments. An appointment belongs to a doctor. Prescription ties all three together.
MongoDB has no joins, so every time I needed data from two collections, I fired multiple queries and pieced the result together myself.
const appointments = await Appointment.find({ patientId: id });
const result = await Promise.all(
appointments.map(async (appt) => {
const doctor = await Doctor.findById(appt.doctorId);
return { ...appt._doc, doctor };
})
);
One query per appointment just to get the doctor's name. A patient with 10 appointments means 11 database round trips. I was doing this across every endpoint in the app.
The same thing in PostgreSQL:
SELECT a.appointment_date, a.status, d.name, d.specialization
FROM appointments a
JOIN doctors d ON a.doctor_id = d.id
WHERE a.patient_id = $1;
One query. When I got this working for the first time, I sat staring at it for a minute because of how much code it replaced.
Problem 3: Two operations with no transaction means broken data is a real possibility
Booking an appointment needs two things to happen together.
- Create the appointment record
- Mark that doctor's slot as unavailable
If the first succeeds and the second fails, the slot still shows as open. Another patient books the same slot. Same doctor, same time, two confirmed appointments.
My workaround was manual rollback logic in application code.
try {
const appointment = await Appointment.create(data);
try {
await Doctor.updateOne(
{ _id: doctorId },
{ $pull: { availableSlots: slot } }
);
} catch (err) {
await Appointment.deleteOne({ _id: appointment._id });
throw err;
}
} catch (err) {
res.status(500).json({ error: "Booking failed" });
}
The cleanup itself can fail. You end up with partial state and no reliable way to detect it.
PostgreSQL handles this with a transaction.
BEGIN;
INSERT INTO appointments (patient_id, doctor_id, slot, status)
VALUES ($1, $2, $3, 'confirmed');
UPDATE doctor_slots
SET is_available = false
WHERE doctor_id = $2 AND slot = $3;
COMMIT;
If anything fails, the whole thing rolls back. No cleanup code needed. No partial state to debug at 2 AM.
What the PostgreSQL schema looked like
Writing the schema upfront felt slow. It also caught three design mistakes I would have discovered much later in application code.
CREATE TABLE patients (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
age INTEGER CHECK (age > 0 AND age < 150),
blood_group VARCHAR(3) CHECK (
blood_group IN ('A+','A-','B+','B-','AB+','AB-','O+','O-')
),
phone VARCHAR(15) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE appointments (
id SERIAL PRIMARY KEY,
patient_id INTEGER REFERENCES patients(id) ON DELETE CASCADE,
doctor_id INTEGER REFERENCES doctors(id),
slot TIMESTAMP NOT NULL,
status VARCHAR(20) DEFAULT 'pending',
UNIQUE(doctor_id, slot)
);
The CHECK on blood_group means the database rejects anything outside those eight values. The three-versions-of-the-same-field problem from MongoDB cannot happen here.
The UNIQUE(doctor_id, slot) makes double booking impossible at the database level. Not something I validate in code. Not something I have to remember to check. The second insert just fails.
Where MongoDB actually fits in healthcare
Not saying avoid it entirely. There are cases where it makes sense.
Unstructured clinical notes where fields genuinely differ per patient. IoT device readings with unpredictable schemas. Medical imaging metadata. These are cases where the data does not have a fixed shape and MongoDB's flexibility is actually useful.
But patient records, appointments, prescriptions, doctor schedules? That data is relational and always has been. Putting it in MongoDB means you end up writing the referential integrity, the transaction logic, and the constraint validation yourself in application code. You are just reinventing the things PostgreSQL gives you by default.
What I would do differently
I wasted two months because I picked a database based on what friends were using instead of looking at the actual shape of my data.
If data has a fixed structure and clear relationships between entities, PostgreSQL is the right call. Schema up front, foreign keys, transactions, joins. None of that has to live in your application code.
The two hours you spend defining the schema will save you from the kind of bugs that only show up when two requests hit the database at the same time, or when one operation out of three fails halfway through.
Top comments (0)