Full-stack medical appointment booking system, built with Ruby on Rails 8. Role-based platform for patients, doctors and admins - with pessimistic locking against double-booking, Redis-cached slot availability, Sidekiq background jobs, and Stimulus.js interactivity.
MediQueue is a full-stack medical appointment booking platform built to solve real-world engineering problems: concurrent slot booking, background job orchestration, Redis caching, and role-based access control.
Patients browse specialists, pick available time slots, book appointments and leave reviews. Doctors manage their schedules and view incoming bookings. Admins have full control over the system through a dedicated panel.
The main engineering focus was on correctness under concurrency - two patients trying to book the same slot at the same time should never both succeed, and the system should handle this gracefully without blocking.
Demo credentials:
| Role | Password | |
|---|---|---|
| Patient | patient@mediqueue.pl | password123 |
| Doctor | kowalski@mediqueue.pl | password123 |
| Admin | admin@mediqueue.pl | password123 |
- User registration and login (Devise)
- Role-based access - Patient, Doctor, Admin (Pundit)
- Browse doctors by specialty with live filtering (Stimulus.js)
- Book appointments from pre-generated time slots
- Pessimistic locking -
FOR UPDATE NOWAITprevents double-booking under concurrent load - Appointment management - cancel with 24h policy, status tracking
- Doctor panel - manage schedule, view appointments, edit profile
- Admin panel - full CRUD on clinics, specialties, doctors, appointments
- Patient reviews per appointment with star rating
- Email notifications - confirmation, reminder, cancellation, weekly report
- Background jobs -
ReminderJob,CleanupJob,ReportJob,SlotGeneratorJob - Redis-cached slot availability (24h TTL, invalidated on schedule change)
- Account settings - change name, email, password, delete account
- Dockerized full-stack environment
- Framework: Ruby on Rails 8.1
- Language: Ruby 3.4
- Database: PostgreSQL 16
- Cache / Queue: Redis 7
- Background Jobs: Sidekiq
- Authentication: Devise
- Authorization: Pundit
- Styling: Tailwind CSS
- Testing: RSpec
- Containerization: Docker, docker-compose
- Email preview: letter_opener_web
Two users attempting to book the same slot simultaneously are handled via PostgreSQL row-level locking. The second transaction fails immediately rather than waiting - which gives the user instant feedback instead of a confusing delay.
Appointment
.where(doctor: @doctor, scheduled_at: @scheduled_at)
.where.not(status: :cancelled)
.lock("FOR UPDATE NOWAIT")
.firstImplemented in AppointmentBookingService which wraps the whole booking flow in a single transaction.
SlotGeneratorJob pre-computes available slots for 60 days ahead and caches them in Redis with a 24h TTL. Slot availability checks never hit PostgreSQL on read - the cache is invalidated and regenerated whenever a doctor changes their schedule.
REDIS.setex(
"doctor:#{doctor_id}:available_slots",
24.hours.to_i,
available_slots.to_json
)| Job | Description | Schedule |
|---|---|---|
ReminderJob |
Sends email 24h before appointment | Every hour |
CleanupJob |
Releases unconfirmed slots older than 15 min | Every 15 minutes |
ReportJob |
Weekly summary report for doctors | Every Monday |
SlotGeneratorJob |
Regenerates Redis cache on schedule change | On demand |
Composite PostgreSQL indexes on the most common query patterns:
idx_appointments_doctor_scheduled: (doctor_id, scheduled_at)
idx_appointments_status_created: (status, created_at)
idx_schedules_doctor_day: (doctor_id, day_of_week)
idx_notifications_user_read: (user_id, read_at)app/
βββ controllers/
β βββ admin/ # Admin panel
β βββ doctors/ # Doctor panel
βββ services/
β βββ appointment_booking_service.rb # Pessimistic locking, booking flow
βββ jobs/
β βββ cleanup_job.rb # Release unconfirmed slots
β βββ reminder_job.rb # 24h email reminders
β βββ report_job.rb # Weekly doctor reports
β βββ slot_generator_job.rb # Cache slot availability in Redis
βββ policies/
β βββ appointment_policy.rb # Pundit authorization rules
βββ mailers/
βββ appointment_mailer.rb # Confirmation, reminder, cancellation
User (Devise)
βββ has_one :doctor_profile (Doctor)
βββ has_many :appointments_as_patient
βββ has_many :appointments_as_doctor
Doctor
βββ belongs_to :user
βββ belongs_to :clinic
βββ belongs_to :specialty
βββ has_many :schedules
βββ has_many :reviews
Appointment
βββ belongs_to :doctor (User)
βββ belongs_to :patient (User)
βββ has_one :review
Schedule # Weekly availability (Mon 9-17, etc.)
Specialty # Cardiology, Dermatology, etc.
Clinic # Multi-tenant data isolation
Review # Patient reviews per appointment
| Role | Capabilities |
|---|---|
| Patient | Browse doctors, book appointments, cancel (more than 24h before), leave reviews |
| Doctor | View appointments, manage weekly schedule, update profile |
| Admin | Full CRUD on clinics, specialties, doctors, appointments |
git clone https://github.com/Rafal5671/MediQueue
cd MediQueue
cp .env.example .env
# Add your RAILS_MASTER_KEY to .env
docker-compose build
docker-compose up
# In a separate terminal
docker-compose exec web bin/rails db:create db:migrate db:seedVisit http://127.0.0.1
git clone https://github.com/Rafal5671/MediQueue
cd MediQueue
bundle install
rails db:create db:migrate db:seed
redis-server &
bundle exec sidekiq &
rails serverbundle exec rspec
bundle exec rspec --format documentationCurrent coverage:
AppointmentBookingService- pessimistic locking, slot taken scenariosCleanupJob- unconfirmed slot release logic- Model validations - User, Appointment, Schedule, Review, Clinic, Specialty
- Mailer specs - AppointmentMailer (confirmation, reminder, cancellation)
All emails are captured by letter_opener_web and available at:
http://127.0.0.1/letter_opener
Emails sent by the system:
- Appointment confirmation - immediately after booking
- Appointment reminder - 24h before the appointment
- Appointment cancellation - when cancelled by patient or doctor
- Weekly report - every Monday, sent to doctors
- Password reset, email changed, password changed - Devise security emails