= Advanced Application Development = == Transactions == Medora's service layer uses **@Transactional** on methods that write to more than one table, so a failure partway through rolls back everything instead of leaving incomplete data behind. Five real examples from the codebase: === Scenario 1: Patient creation with medical record === '''File:''' `src/main/java/medora/service/PatientService.java` Creates a patient and automatically creates their medical record in the same transaction. {{{ @Transactional public Patient createPatient(Patient patient) { if (patient == null || patient.getEmbg() == null || patient.getEmbg().isBlank()) { throw new IllegalArgumentException("Patient EMBG is required"); } if (patient.getFirstName() == null || patient.getFirstName().isBlank()) { throw new IllegalArgumentException("Patient first name is required"); } if (patient.getLastName() == null || patient.getLastName().isBlank()) { throw new IllegalArgumentException("Patient last name is required"); } logger.info("Creating new patient with EMBG: {}", patient.getEmbg()); Patient savedPatient = patientRepository.save(patient); try { MedicalRecord medicalRecord = new MedicalRecord(); medicalRecord.setPatient(savedPatient); medicalRecordRepository.save(medicalRecord); logger.info("Created medical record for patient ID: {}", savedPatient.getPatientId()); } catch (Exception e) { logger.error("Failed to create medical record for patient: {}", e.getMessage()); } return savedPatient; } }}} '''Transaction behavior:''' both `patientRepository.save()` and `medicalRecordRepository.save()` execute within the same transaction. If either fails, both are rolled back. === Scenario 2: Auto generate billing from procedures & lab tests === '''File:''' `src/main/java/medora/service/BillingService.java` Creates a billing record and links every performed procedure and lab test for a patient on a given date. {{{ @Transactional public void autoGenerateBillingForPatientService(Long patientId, LocalDate serviceDate) { try { if (patientId == null || patientId <= 0) { throw new IllegalArgumentException("Patient ID must be valid"); } if (serviceDate == null) { throw new IllegalArgumentException("Service date must be valid"); } logger.info("Starting auto-billing for patient {} on date {}", patientId, serviceDate); MedicalRecord medicalRecord = medicalRecordRepository.findByPatientPatientId(patientId) .orElseGet(() -> { logger.info("Creating new medical record for patient {}", patientId); MedicalRecord newRecord = new MedicalRecord(); newRecord.setPatient(patientRepository.findById(patientId) .orElseThrow(() -> new RuntimeException("Patient not found with ID: " + patientId))); return medicalRecordRepository.save(newRecord); }); logger.info("Using medical record {} for patient {}", medicalRecord.getRecordId(), patientId); List procedures = performedProcedureRepository.findByPatientAndDate(patientId, serviceDate); List labTests = performedLabTestRepository.findByPatientAndDate(patientId, serviceDate); logger.info("Found {} procedures and {} lab tests for patient {} on {}", procedures.size(), labTests.size(), patientId, serviceDate); if (procedures.isEmpty() && labTests.isEmpty()) { logger.info("No procedures or lab tests found for patient {} on {}", patientId, serviceDate); return; } BigDecimal procedureCost = procedures.stream() .map(p -> { BigDecimal cost = p.getProcedure().getCost(); logger.debug("Procedure {} cost: {}", p.getProcedure().getProcedureId(), cost); return cost; }) .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal labTestCost = labTests.stream() .map(lt -> { BigDecimal cost = lt.getLabTest().getCost(); logger.debug("Lab test {} cost: {}", lt.getLabTest().getTestId(), cost); return cost; }) .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal totalCost = procedureCost.add(labTestCost); logger.info("Total cost calculation: procedures={}, labTests={}, total={}", procedureCost, labTestCost, totalCost); // Get default admin (first admin in system) - skip billing if none found Optional adminOptional = adminRepository.findAll() .stream() .findFirst(); if (adminOptional.isEmpty()) { logger.warn("No admin found in system - skipping automatic billing generation for patient {} on {}", patientId, serviceDate); return; } Admin admin = adminOptional.get(); Billing billing = billingRepository.findBillingForPatientOnDate(patientId, serviceDate); if (billing != null) { logger.info("Billing record {} already exists for patient {} on {}, updating with new total", billing.getBillId(), patientId, serviceDate); billing.setTotalCost(totalCost); } else { billing = new Billing(); billing.setMedicalRecord(medicalRecord); billing.setAdmin(admin); billing.setTotalCost(totalCost); billing.setPaymentStatus(PaymentStatus.PENDING); billing.setPaymentDate(serviceDate); logger.info("Creating new billing record for patient {} on {}", patientId, serviceDate); } Billing savedBilling = billingRepository.save(billing); logger.info("Billing record {} for patient {} on {} with total cost: {}", savedBilling.getBillId(), patientId, serviceDate, totalCost); for (PerformedProcedures procedure : procedures) { try { BillingProcedures billingProcedure = new BillingProcedures(savedBilling, procedure.getProcedure()); billingProceduresRepository.save(billingProcedure); logger.debug("Linked procedure {} to billing {}", procedure.getProcedure().getProcedureId(), savedBilling.getBillId()); } catch (Exception e) { logger.debug("Procedure {} already linked to billing {}", procedure.getProcedure().getProcedureId(), savedBilling.getBillId()); } } for (PerformedLabTests labTest : labTests) { try { BillingLabTests billingLabTest = new BillingLabTests(savedBilling, labTest.getLabTest()); billingLabTestsRepository.save(billingLabTest); logger.debug("Linked lab test {} to billing {}", labTest.getLabTest().getTestId(), savedBilling.getBillId()); } catch (Exception e) { logger.debug("Lab test {} already linked to billing {}", labTest.getLabTest().getTestId(), savedBilling.getBillId()); } } logger.info("Successfully processed {} procedures and {} lab tests for billing record {}", procedures.size(), labTests.size(), savedBilling.getBillId()); } catch (Exception e) { logger.error("Error in auto-billing for patient {} on date {}: {}", patientId, serviceDate, e.getMessage(), e); throw e; } } }}} **Transaction behavior**: Medical record lookup or creation, cost calculation, billing creation or update and linking every procedure and lab test all happen in one transaction. If any step fails, everything rolls back so no half created billing record and no partially linked items. === Scenario 3: Request a lab test with automated billing === '''File:''' `src/main/java/medora/service/LabService.java` A doctor requests a lab test for a patient, which automatically triggers billing generation. {{{ @Transactional public PerformedLabTests requestLabTestForPatient(Long patientId, Long doctorId, Long testId, LocalDate testDate, String notes) { if (patientId == null || patientId <= 0) throw new IllegalArgumentException("Invalid patient ID"); if (doctorId == null || doctorId <= 0) throw new IllegalArgumentException("Invalid doctor ID"); if (testId == null || testId <= 0) throw new IllegalArgumentException("Invalid test ID"); Patient patient = patientRepository.findById(patientId) .orElseThrow(() -> new RuntimeException("Patient not found")); Doctors doctor = doctorRepository.findById(doctorId) .orElseThrow(() -> new RuntimeException("Doctor not found")); LabTests test = labTestRepository.findById(testId) .orElseThrow(() -> new RuntimeException("Lab test not found")); PerformedLabTests performedTest = new PerformedLabTests(); performedTest.setPatient(patient); performedTest.setDoctor(doctor); performedTest.setLabTest(test); LocalDate finalTestDate = testDate != null ? testDate : LocalDate.now(); performedTest.setTestDate(finalTestDate); performedTest.setNotes(notes); logger.info("Lab test {} requested for patient {} by doctor {}", testId, patientId, doctorId); PerformedLabTests saved = performedLabTestRepository.save(performedTest); // Auto generate billing for the patient on this date billingService.autoGenerateBillingForPatientService(patientId, finalTestDate); return saved; } }}} '''Transaction behavior:''' the lab test save and the billing service call execute in a single transaction. If billing generation fails, the lab test creation is rolled back. === Scenario 4: Store lab result and link to medical record === '''File:''' `src/main/java/medora/service/LabService.java` Stores a lab result and links it to the patient's medical record. {{{ @Transactional public MedicalRecordLabResults storeLabResult(Long medicalRecordId, Long testId, String results, LocalDate resultDate) { if (medicalRecordId == null || medicalRecordId <= 0) throw new IllegalArgumentException("Invalid medical record ID"); if (testId == null || testId <= 0) throw new IllegalArgumentException("Invalid test ID"); if (results == null || results.isBlank()) throw new IllegalArgumentException("Results required"); MedicalRecord medicalRecord = medicalRecordRepository.findById(medicalRecordId) .orElseThrow(() -> new RuntimeException("Medical record not found")); LabTests labTest = labTestRepository.findById(testId) .orElseThrow(() -> new RuntimeException("Lab test not found")); LabResults labResult = new LabResults(); labResult.setResults(results); labResult.setResultDate(resultDate); labResult.setLabTest(labTest); LabResults saved = labResultsRepository.save(labResult); // Use the join repository to safely link to avoid deleting existing links // Create and save the join entity explicitly MedicalRecordLabResults link = new MedicalRecordLabResults(); link.setMedicalRecord(medicalRecord); link.setLabResult(saved); logger.info("Stored lab result {} for medical record {}", saved.getResultId(), medicalRecordId); return medicalRecordLabResultRepository.save(link); } }}} '''Transaction behavior:''' Both the lab result creation and the link to the medical record happen atomically. If either fails, both are rolled back. === Scenario 5: Appointment creation === '''File:''' `src/main/java/medora/service/AppointmentService.java` Creates an appointment with validation for future dates, no double-booking, and no duplicate appointments. {{{ @Transactional public Appointment createAppointment(Long patientId, Long doctorId, LocalDate appointmentDate, LocalTime appointmentTime) { if (patientId == null || patientId <= 0) { throw new IllegalArgumentException("Patient ID must be valid"); } if (doctorId == null || doctorId <= 0) { throw new IllegalArgumentException("Doctor ID must be valid"); } if (appointmentDate == null) { throw new IllegalArgumentException("Appointment date is required"); } if (appointmentTime == null) { throw new IllegalArgumentException("Appointment time is required"); } Patient patient = patientRepository.findById(patientId) .orElseThrow(() -> new RuntimeException("Patient not found with ID: " + patientId)); Doctors doctor = doctorRepository.findById(doctorId) .orElseThrow(() -> new RuntimeException("Doctor not found with ID: " + doctorId)); LocalDateTime appointmentDateTime = LocalDateTime.of(appointmentDate, appointmentTime); // Future validation if (!appointmentDateTime.isAfter(LocalDateTime.now())) { throw new RuntimeException( "Appointment must be scheduled for a future date and time" ); } // Doctor slot validation boolean doctorBusy = appointmentRepository .existsByDoctorDoctorIdAndAppointmentDateAndAppointmentTimeAndStatusNot( doctorId, appointmentDate, appointmentTime, AppointmentStatus.CANCELLED ); if (doctorBusy) { throw new RuntimeException("This appointment slot is already booked"); } // Duplicate patient validation boolean duplicateAppointment = appointmentRepository .existsByPatientPatientIdAndDoctorDoctorIdAndAppointmentDateAndAppointmentTimeAndStatusNot( patientId, doctorId, appointmentDate, appointmentTime, AppointmentStatus.CANCELLED ); if (duplicateAppointment) { throw new RuntimeException( "Patient already has this appointment scheduled" ); } Appointment appointment = new Appointment(); appointment.setPatient(patient); appointment.setDoctor(doctor); appointment.setAppointmentDate(appointmentDate); appointment.setAppointmentTime(appointmentTime); appointment.setStatus(AppointmentStatus.SCHEDULED); logger.info( "Creating appointment for patient ID: {} with doctor ID: {}", patientId, doctorId ); return appointmentRepository.save(appointment); } }}} '''Transaction behavior:''' Validation queries and appointment creation all happen within a single transaction. ---- == Database Connection Pooling == === Current configuration === `src/main/resources/application.properties` defines the datasource, plus the explicit HikariCP pool settings: {{{ spring.datasource.url=jdbc:postgresql://localhost:5432/medora spring.datasource.username=postgres spring.datasource.password=${DB_PASSWORD} spring.datasource.driver-class-name=org.postgresql.Driver # JPA / Hibernate settings spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect # HikariCP connection pooling configuration spring.datasource.hikari.maximum-pool-size=20 spring.datasource.hikari.minimum-idle=5 spring.datasource.hikari.connection-timeout=30000 spring.datasource.hikari.idle-timeout=600000 spring.datasource.hikari.max-lifetime=1800000 spring.datasource.hikari.leak-detection-threshold=60000 }}} Spring Boot automatically configures HikariCP as the connection pool once a datasource is present; the block above makes pool sizing and timeout behavior explicit rather than relying on defaults. === Connection pool in use === Real startup and shutdown log output from the running application: '''Startup:''' {{{ 2026-08-30T13:22:28.551+02:00 INFO 37848 --- [medora] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... 2026-08-30T13:22:28.673+02:00 INFO 37848 --- [medora] [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@6ac756b 2026-08-30T13:22:28.673+02:00 INFO 37848 --- [medora] [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@3f8c92e1 2026-08-30T13:22:28.675+02:00 INFO 37848 --- [medora] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed. }}} '''Shutdown:''' {{{ 2026-08-30T13:22:31.457+02:00 INFO 37848 --- [medora] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated... 2026-08-30T13:22:31.461+02:00 INFO 37848 --- [medora] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed. }}} This shows the full pool lifecycle. The pool initializes on startup and then connections are created and added, as shown with real PostgreSQL connection object references, the pool signals it's ready, and on shutdown it closes gracefully and releases its connections. With **minimum-idle=5**, additional connections are created up to that threshold during startup. Each service method annotated **@Transactional** obtains a connection from this pool for the duration of its transaction, then returns it for reuse.