Changes between Version 1 and Version 2 of AdvancedApplicationDevelopment
- Timestamp:
- 09/23/26 23:25:56 (5 hours ago)
Legend:
- Unmodified
- Added
- Removed
- Modified
-
AdvancedApplicationDevelopment
v1 v2 3 3 == Transactions == 4 4 5 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: 6 7 === Scenario 1: Patient creation with medical record === 8 9 '''File:''' `src/main/java/medora/service/PatientService.java` 10 11 Creates a patient and automatically creates their medical record in the same transaction. 5 Medora's service layer uses **@Transactional** on methods that perform multiple related database operations. When an unchecked exception is propagated from a transactional method, Spring rolls back the database changes made during that transaction instead of leaving incomplete data behind. 6 7 The PostgreSQL database also enforces important business rules through foreign keys, custom domains, triggers, views, and stored procedures. Therefore, transaction and data-integrity rules are enforced at both the application and database levels. 8 9 === Scenario 1: Patient creation with user account and medical record === 10 11 '''File:''' `backend/src/main/java/medora/service/PatientService.java` 12 13 Patient creation creates a login account, a patient profile, and a medical record in one transaction. 12 14 13 15 {{{ 14 16 @Transactional 15 public Patient createPatient(Patient patient ) {17 public Patient createPatient(Patient patient, String rawPassword) { 16 18 if (patient == null || patient.getEmbg() == null || patient.getEmbg().isBlank()) { 17 19 throw new IllegalArgumentException("Patient EMBG is required"); … … 23 25 throw new IllegalArgumentException("Patient last name is required"); 24 26 } 27 if (rawPassword == null || rawPassword.isBlank()) { 28 throw new IllegalArgumentException("Password is required"); 29 } 30 31 if (userRepository.existsByUsername(patient.getEmbg())) { 32 throw new RuntimeException("A user account for this EMBG already exists"); 33 } 34 35 User user = new User(); 36 user.setUsername(patient.getEmbg()); 37 user.setPassword(passwordEncoder.encode(rawPassword)); 38 user.setRole("PATIENT"); 39 user.setFirstName(patient.getFirstName()); 40 user.setLastName(patient.getLastName()); 41 user.setIsActive(true); 42 43 User savedUser = userRepository.save(user); 44 45 patient.setUser(savedUser); 46 patient.setPatientId(patientRepository.findMaxPatientId() + 1); 25 47 26 48 logger.info("Creating new patient with EMBG: {}", patient.getEmbg()); 27 49 Patient savedPatient = patientRepository.save(patient); 28 50 29 30 51 try { 31 52 MedicalRecord medicalRecord = new MedicalRecord(); 53 medicalRecord.setRecordId(medicalRecordRepository.findMaxRecordId() + 1); 32 54 medicalRecord.setPatient(savedPatient); 33 55 medicalRecordRepository.save(medicalRecord); 34 logger.info("Created medical record for patient ID: {}", savedPatient.getPatientId()); 56 57 logger.info("Created medical record for patient ID: {}", 58 savedPatient.getPatientId()); 35 59 } catch (Exception e) { 36 logger.error("Failed to create medical record for patient: {}", e.getMessage()); 60 logger.error("Failed to create medical record for patient {}", 61 savedPatient.getPatientId(), e); 62 throw e; 37 63 } 38 64 … … 41 67 }}} 42 68 43 '''Transaction behavior:''' both `patientRepository.save()` and `medicalRecordRepository.save()` execute within the same transaction. If either fails, both are rolled back. 69 '''Transaction behavior:''' The user, patient, and medical-record inserts execute within the same transaction. If medical-record creation fails, the exception is logged and rethrown, allowing Spring to roll back the transaction. 70 71 The `patients.user_id` foreign key is mandatory, so the user account must be saved before the patient profile. 44 72 45 73 === Scenario 2: Auto generate billing from procedures & lab tests === 46 74 47 '''File:''' ` src/main/java/medora/service/BillingService.java`48 49 Creates a billing record and links every performed procedure and lab test for a patient on a givendate.75 '''File:''' `backend/src/main/java/medora/service/BillingService.java` 76 77 This service creates or updates a billing record and links performed procedures and laboratory tests for a patient on a particular date. 50 78 51 79 {{{ 52 80 @Transactional 53 public void autoGenerateBillingForPatientService(Long patientId, LocalDate serviceDate) { 81 public void autoGenerateBillingForPatientService(Long patientId, 82 LocalDate serviceDate) { 54 83 try { 55 84 if (patientId == null || patientId <= 0) { 56 85 throw new IllegalArgumentException("Patient ID must be valid"); 57 86 } 87 58 88 if (serviceDate == null) { 59 89 throw new IllegalArgumentException("Service date must be valid"); 60 90 } 61 91 62 logger.info("Starting auto-billing for patient {} on date {}", patientId, serviceDate); 63 64 MedicalRecord medicalRecord = medicalRecordRepository.findByPatientPatientId(patientId) 65 .orElseGet(() -> { 66 logger.info("Creating new medical record for patient {}", patientId); 67 MedicalRecord newRecord = new MedicalRecord(); 68 newRecord.setPatient(patientRepository.findById(patientId) 69 .orElseThrow(() -> new RuntimeException("Patient not found with ID: " + patientId))); 70 return medicalRecordRepository.save(newRecord); 71 }); 72 73 logger.info("Using medical record {} for patient {}", medicalRecord.getRecordId(), patientId); 74 75 List<PerformedProcedures> procedures = performedProcedureRepository.findByPatientAndDate(patientId, serviceDate); 76 List<PerformedLabTests> labTests = performedLabTestRepository.findByPatientAndDate(patientId, serviceDate); 92 logger.info("Starting auto-billing for patient {} on date {}", 93 patientId, serviceDate); 94 95 MedicalRecord medicalRecord = 96 medicalRecordRepository.findByPatientPatientId(patientId) 97 .orElseGet(() -> { 98 logger.info("Creating new medical record for patient {}", 99 patientId); 100 101 MedicalRecord newRecord = new MedicalRecord(); 102 newRecord.setRecordId( 103 medicalRecordRepository.findMaxRecordId() + 1); 104 105 newRecord.setPatient( 106 patientRepository.findById(patientId) 107 .orElseThrow(() -> 108 new RuntimeException( 109 "Patient not found with ID: " 110 + patientId))); 111 112 return medicalRecordRepository.save(newRecord); 113 }); 114 115 logger.info("Using medical record {} for patient {}", 116 medicalRecord.getRecordId(), patientId); 117 118 List<PerformedProcedures> procedures = 119 performedProcedureRepository.findByPatientAndDate( 120 patientId, serviceDate); 121 122 List<PerformedLabTests> labTests = 123 performedLabTestRepository.findByPatientAndDate( 124 patientId, serviceDate); 77 125 78 126 logger.info("Found {} procedures and {} lab tests for patient {} on {}", … … 80 128 81 129 if (procedures.isEmpty() && labTests.isEmpty()) { 82 logger.info("No procedures or lab tests found for patient {} on {}", patientId, serviceDate); 130 logger.info("No procedures or lab tests found for patient {} on {}", 131 patientId, serviceDate); 83 132 return; 84 133 } … … 87 136 .map(p -> { 88 137 BigDecimal cost = p.getProcedure().getCost(); 89 logger.debug("Procedure {} cost: {}", p.getProcedure().getProcedureId(), cost); 138 139 logger.debug("Procedure {} cost: {}", 140 p.getProcedure().getProcedureId(), cost); 141 90 142 return cost; 91 143 }) … … 95 147 .map(lt -> { 96 148 BigDecimal cost = lt.getLabTest().getCost(); 97 logger.debug("Lab test {} cost: {}", lt.getLabTest().getTestId(), cost); 149 150 logger.debug("Lab test {} cost: {}", 151 lt.getLabTest().getTestId(), cost); 152 98 153 return cost; 99 154 }) … … 101 156 102 157 BigDecimal totalCost = procedureCost.add(labTestCost); 103 logger.info("Total cost calculation: procedures={}, labTests={}, total={}", procedureCost, labTestCost, totalCost); 104 105 // Get default admin (first admin in system) - skip billing if none found 158 159 logger.info("Total cost calculation: procedures={}, labTests={}, total={}", 160 procedureCost, labTestCost, totalCost); 161 106 162 Optional<Admin> adminOptional = adminRepository.findAll() 107 163 .stream() … … 109 165 110 166 if (adminOptional.isEmpty()) { 111 logger.warn("No admin found in system - skipping automatic billing generation for patient {} on {}", patientId, serviceDate); 167 logger.warn("No admin found in system - skipping automatic billing " 168 + "generation for patient {} on {}", 169 patientId, serviceDate); 112 170 return; 113 171 } … … 115 173 Admin admin = adminOptional.get(); 116 174 117 Billing billing = billingRepository.findBillingForPatientOnDate(patientId, serviceDate); 175 Billing billing = billingRepository.findBillingForPatientOnDate( 176 patientId, serviceDate); 118 177 119 178 if (billing != null) { 120 logger.info("Billing record {} already exists for patient {} on {}, updating with new total", 179 logger.info("Billing record {} already exists for patient {} on {}, " 180 + "updating with new total", 121 181 billing.getBillId(), patientId, serviceDate); 182 122 183 billing.setTotalCost(totalCost); 123 184 } else { 124 185 billing = new Billing(); 186 billing.setBillId(billingRepository.findMaxBillId() + 1); 125 187 billing.setMedicalRecord(medicalRecord); 126 188 billing.setAdmin(admin); … … 129 191 billing.setPaymentDate(serviceDate); 130 192 131 logger.info("Creating new billing record for patient {} on {}", patientId, serviceDate); 193 logger.info("Creating new billing record for patient {} on {}", 194 patientId, serviceDate); 132 195 } 133 196 134 197 Billing savedBilling = billingRepository.save(billing); 198 135 199 logger.info("Billing record {} for patient {} on {} with total cost: {}", 136 200 savedBilling.getBillId(), patientId, serviceDate, totalCost); 137 201 138 202 for (PerformedProcedures procedure : procedures) { 139 try { 140 BillingProcedures billingProcedure = new BillingProcedures(savedBilling, procedure.getProcedure()); 203 if (!billingProceduresRepository 204 .existsByBillingBillIdAndProcedureProcedureId( 205 savedBilling.getBillId(), 206 procedure.getProcedure().getProcedureId())) { 207 208 BillingProcedures billingProcedure = 209 new BillingProcedures( 210 savedBilling, 211 procedure.getProcedure()); 212 141 213 billingProceduresRepository.save(billingProcedure); 142 logger.debug("Linked procedure {} to billing {}", procedure.getProcedure().getProcedureId(), savedBilling.getBillId()); 143 } catch (Exception e) { 144 logger.debug("Procedure {} already linked to billing {}", procedure.getProcedure().getProcedureId(), savedBilling.getBillId()); 214 215 logger.debug("Linked procedure {} to billing {}", 216 procedure.getProcedure().getProcedureId(), 217 savedBilling.getBillId()); 145 218 } 146 219 } 147 220 148 221 for (PerformedLabTests labTest : labTests) { 149 try { 150 BillingLabTests billingLabTest = new BillingLabTests(savedBilling, labTest.getLabTest()); 222 if (!billingLabTestsRepository 223 .existsByBillingBillIdAndLabTestTestId( 224 savedBilling.getBillId(), 225 labTest.getLabTest().getTestId())) { 226 227 BillingLabTests billingLabTest = 228 new BillingLabTests( 229 savedBilling, 230 labTest.getLabTest()); 231 151 232 billingLabTestsRepository.save(billingLabTest); 152 logger.debug("Linked lab test {} to billing {}", labTest.getLabTest().getTestId(), savedBilling.getBillId()); 153 } catch (Exception e) { 154 logger.debug("Lab test {} already linked to billing {}", labTest.getLabTest().getTestId(), savedBilling.getBillId()); 233 234 logger.debug("Linked lab test {} to billing {}", 235 labTest.getLabTest().getTestId(), 236 savedBilling.getBillId()); 155 237 } 156 238 } 157 239 158 logger.info("Successfully processed {} procedures and {} lab tests for billing record {}", 159 procedures.size(), labTests.size(), savedBilling.getBillId()); 240 logger.info("Successfully processed {} procedures and {} lab tests " 241 + "for billing record {}", 242 procedures.size(), 243 labTests.size(), 244 savedBilling.getBillId()); 160 245 161 246 } catch (Exception e) { 162 logger.error("Error in auto-billing for patient {} on date {}: {}", patientId, serviceDate, e.getMessage(), e); 247 logger.error("Error in auto-billing for patient {} on date {}: {}", 248 patientId, serviceDate, e.getMessage(), e); 163 249 throw e; 164 250 } … … 166 252 }}} 167 253 168 **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. 169 254 '''Transaction behavior:''' Medical-record lookup or creation, billing creation or update, and billing-link creation all execute within one transaction. If an unchecked exception is propagated, the transaction can be rolled back. 255 256 The application calculates and initially assigns the total. However, the database triggers `trg_billing_procedures_update_total` and `trg_billing_lab_tests_update_total` recalculate `billing.total_cost` from the linked line items. Therefore, the database calculation is authoritative after the billing links are inserted. 170 257 171 258 === Scenario 3: Request a lab test with automated billing === 172 259 173 '''File:''' ` src/main/java/medora/service/LabService.java`174 175 A doctor requests a lab test for a patient, which automatically triggers billing generation.260 '''File:''' `backend/src/main/java/medora/service/LabService.java` 261 262 A doctor requests a laboratory test for a patient. The request also identifies the laboratory technician and automatically starts billing for the same patient and date. 176 263 177 264 {{{ 178 265 @Transactional 179 266 public PerformedLabTests requestLabTestForPatient(Long patientId, 180 Long doctorId, 181 Long testId, 182 LocalDate testDate, 183 String notes) { 184 if (patientId == null || patientId <= 0) 267 Long doctorId, 268 Long testId, 269 Long technicianId, 270 LocalDate testDate, 271 String notes) { 272 if (patientId == null || patientId <= 0) { 185 273 throw new IllegalArgumentException("Invalid patient ID"); 186 187 if (doctorId == null || doctorId <= 0) 274 } 275 276 if (doctorId == null || doctorId <= 0) { 188 277 throw new IllegalArgumentException("Invalid doctor ID"); 189 190 if (testId == null || testId <= 0) 278 } 279 280 if (testId == null || testId <= 0) { 191 281 throw new IllegalArgumentException("Invalid test ID"); 282 } 283 284 if (technicianId == null || technicianId <= 0) { 285 throw new IllegalArgumentException("Invalid lab technician ID"); 286 } 192 287 193 288 Patient patient = patientRepository.findById(patientId) … … 200 295 .orElseThrow(() -> new RuntimeException("Lab test not found")); 201 296 297 LabTechnician technician = labTechnicianRepository.findById(technicianId) 298 .orElseThrow(() -> 299 new RuntimeException("Lab technician not found")); 300 202 301 PerformedLabTests performedTest = new PerformedLabTests(); 302 303 performedTest.setPerformedTestId( 304 performedLabTestRepository.findMaxPerformedTestId() + 1); 305 203 306 performedTest.setPatient(patient); 204 307 performedTest.setDoctor(doctor); 205 308 performedTest.setLabTest(test); 206 LocalDate finalTestDate = testDate != null ? testDate : LocalDate.now(); 309 performedTest.setTechnician(technician); 310 311 LocalDate finalTestDate = 312 testDate != null ? testDate : LocalDate.now(); 313 207 314 performedTest.setTestDate(finalTestDate); 208 315 performedTest.setNotes(notes); 209 316 210 logger.info("Lab test {} requested for patient {} by doctor {}", testId, patientId, doctorId); 211 PerformedLabTests saved = performedLabTestRepository.save(performedTest); 212 213 // Auto generate billing for the patient on this date 214 billingService.autoGenerateBillingForPatientService(patientId, finalTestDate); 317 logger.info("Lab test {} requested for patient {} by doctor {}", 318 testId, patientId, doctorId); 319 320 PerformedLabTests saved = 321 performedLabTestRepository.save(performedTest); 322 323 billingService.autoGenerateBillingForPatientService( 324 patientId, finalTestDate); 215 325 216 326 return saved; … … 218 328 }}} 219 329 220 '''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. 221 222 === Scenario 4: Store lab result and link to medical record === 223 224 '''File:''' `src/main/java/medora/service/LabService.java` 225 226 Stores a lab result and links it to the patient's medical record. 330 '''Transaction behavior:''' The performed laboratory-test record and automatic billing operation execute within the same transaction. If billing generation fails and the exception is propagated, the laboratory-test request is rolled back. 331 332 The `performed_lab_tests.technician_id` column is mandatory in the database, and the Java entity reflects this: 333 334 {{{ 335 @ManyToOne(optional = false, fetch = FetchType.LAZY) 336 @JoinColumn(name = "technician_id", nullable = false) 337 private LabTechnician technician; 338 }}} 339 340 === Scenario 4: Store lab result and link to medical record === 341 342 '''File:''' `backend/src/main/java/medora/service/LabService.java` 343 344 The method creates a laboratory result and links it to a medical record through the `medical_record_lab_results` join table. 227 345 228 346 {{{ 229 347 @Transactional 230 348 public MedicalRecordLabResults storeLabResult(Long medicalRecordId, 231 Long testId, 232 String results, 233 LocalDate resultDate) { 234 235 if (medicalRecordId == null || medicalRecordId <= 0) 236 throw new IllegalArgumentException("Invalid medical record ID"); 237 238 if (testId == null || testId <= 0) 349 Long testId, 350 String results, 351 LocalDate resultDate) { 352 if (medicalRecordId == null || medicalRecordId <= 0) { 353 throw new IllegalArgumentException( 354 "Invalid medical record ID"); 355 } 356 357 if (testId == null || testId <= 0) { 239 358 throw new IllegalArgumentException("Invalid test ID"); 240 241 if (results == null || results.isBlank()) 359 } 360 361 if (results == null || results.isBlank()) { 242 362 throw new IllegalArgumentException("Results required"); 243 244 MedicalRecord medicalRecord = medicalRecordRepository.findById(medicalRecordId) 245 .orElseThrow(() -> new RuntimeException("Medical record not found")); 363 } 364 365 LocalDate finalResultDate = 366 resultDate != null ? resultDate : LocalDate.now(); 367 368 if (finalResultDate.isAfter(LocalDate.now())) { 369 throw new IllegalArgumentException( 370 "Result date cannot be in the future"); 371 } 372 373 MedicalRecord medicalRecord = 374 medicalRecordRepository.findById(medicalRecordId) 375 .orElseThrow(() -> 376 new RuntimeException( 377 "Medical record not found")); 246 378 247 379 LabTests labTest = labTestRepository.findById(testId) 248 .orElseThrow(() -> new RuntimeException("Lab test not found")); 380 .orElseThrow(() -> 381 new RuntimeException("Lab test not found")); 249 382 250 383 LabResults labResult = new LabResults(); 384 labResult.setResultId(labResultsRepository.findMaxResultId() + 1); 251 385 labResult.setResults(results); 252 labResult.setResultDate( resultDate);386 labResult.setResultDate(finalResultDate); 253 387 labResult.setLabTest(labTest); 254 388 255 389 LabResults saved = labResultsRepository.save(labResult); 256 257 // Use the join repository to safely link to avoid deleting existing links258 // Create and save the join entity explicitly259 390 260 391 MedicalRecordLabResults link = new MedicalRecordLabResults(); … … 262 393 link.setLabResult(saved); 263 394 264 logger.info("Stored lab result {} for medical record {}", saved.getResultId(), medicalRecordId); 395 logger.info("Stored lab result {} for medical record {}", 396 saved.getResultId(), medicalRecordId); 397 265 398 return medicalRecordLabResultRepository.save(link); 266 399 } 267 400 }}} 268 401 269 '''Transaction behavior:''' Both the lab result creation and the link to the medical record happen atomically. If either fails, both are rolled back. 402 '''Transaction behavior:''' The laboratory-result insert and medical-record link execute within one transaction. If either operation fails, the exception is propagated and the transaction can roll back. 403 404 The service supplies today's date when no result date is provided and rejects future result dates. This corresponds to the database rule: 405 406 {{{ 407 result_date DATE NOT NULL 408 CHECK (result_date <= CURRENT_DATE) 409 }}} 270 410 271 411 === Scenario 5: Appointment creation === 272 412 273 '''File:''' ` src/main/java/medora/service/AppointmentService.java`274 275 Creates an appointment with validation for future dates, no double-booking, and no duplicate appointments.413 '''File:''' `backend/src/main/java/medora/service/AppointmentService.java` 414 415 Appointment creation validates the patient, doctor, date, time, and availability before saving the appointment. 276 416 277 417 {{{ … … 281 421 LocalDate appointmentDate, 282 422 LocalTime appointmentTime) { 283 284 423 if (patientId == null || patientId <= 0) { 285 424 throw new IllegalArgumentException("Patient ID must be valid"); … … 291 430 292 431 if (appointmentDate == null) { 293 throw new IllegalArgumentException("Appointment date is required"); 432 throw new IllegalArgumentException( 433 "Appointment date is required"); 294 434 } 295 435 296 436 if (appointmentTime == null) { 297 throw new IllegalArgumentException("Appointment time is required"); 437 throw new IllegalArgumentException( 438 "Appointment time is required"); 298 439 } 299 440 300 441 Patient patient = patientRepository.findById(patientId) 301 442 .orElseThrow(() -> 302 new RuntimeException("Patient not found with ID: " + patientId)); 443 new RuntimeException( 444 "Patient not found with ID: " + patientId)); 303 445 304 446 Doctors doctor = doctorRepository.findById(doctorId) 305 447 .orElseThrow(() -> 306 new RuntimeException("Doctor not found with ID: " + doctorId)); 448 new RuntimeException( 449 "Doctor not found with ID: " + doctorId)); 307 450 308 451 LocalDateTime appointmentDateTime = 309 452 LocalDateTime.of(appointmentDate, appointmentTime); 310 453 311 // Future validation312 313 454 if (!appointmentDateTime.isAfter(LocalDateTime.now())) { 314 455 throw new RuntimeException( 315 "Appointment must be scheduled for a future date and time" 316 ); 317 } 318 319 // Doctor slot validation 456 "Appointment must be scheduled for a future date and time"); 457 } 458 320 459 boolean doctorBusy = 321 460 appointmentRepository … … 324 463 appointmentDate, 325 464 appointmentTime, 326 AppointmentStatus.CANCELLED 327 ); 465 AppointmentStatus.CANCELLED); 328 466 329 467 if (doctorBusy) { 330 throw new RuntimeException( "This appointment slot is already booked");331 }332 333 // Duplicate patient validation 468 throw new RuntimeException( 469 "This appointment slot is already booked"); 470 } 471 334 472 boolean duplicateAppointment = 335 473 appointmentRepository … … 339 477 appointmentDate, 340 478 appointmentTime, 341 AppointmentStatus.CANCELLED 342 ); 479 AppointmentStatus.CANCELLED); 343 480 344 481 if (duplicateAppointment) { 345 482 throw new RuntimeException( 346 "Patient already has this appointment scheduled" 347 ); 483 "Patient already has this appointment scheduled"); 348 484 } 349 485 350 486 Appointment appointment = new Appointment(); 351 487 488 appointment.setAppointmentId( 489 appointmentRepository.findMaxAppointmentId() + 1); 352 490 appointment.setPatient(patient); 353 491 appointment.setDoctor(doctor); … … 359 497 "Creating appointment for patient ID: {} with doctor ID: {}", 360 498 patientId, 361 doctorId 362 ); 499 doctorId); 363 500 364 501 return appointmentRepository.save(appointment); … … 366 503 }}} 367 504 368 '''Transaction behavior:''' Validation queries and appointment creation all happen within a single transaction. 505 The application performs basic validation for future dates and duplicate appointment slots. The database triggers provide additional protection by enforcing: 506 507 * no appointments in the past 508 * no overlapping 30-minute appointments for the same doctor 509 * no overlapping 30-minute appointments for the same patient 510 * valid appointment-status transitions 511 * no completion before the scheduled time 512 513 The Java enum matches the database-supported statuses: 514 515 {{{ 516 public enum AppointmentStatus { 517 SCHEDULED, 518 COMPLETED, 519 CANCELLED, 520 IN_PROGRESS, 521 NO_SHOW 522 } 523 }}} 524 525 The database procedure `job_mark_no_show()` changes appointments that remain `SCHEDULED` more than 45 minutes after their scheduled time to `NO_SHOW`. 526 527 '''Transaction behavior:''' Appointment validation queries and appointment creation execute within one transaction. Database triggers provide a second layer of protection against invalid data and overlapping appointments. 369 528 370 529 ---- 371 372 == Database Connection Pooling ==373 374 530 === Current configuration === 375 531 … … 377 533 378 534 {{{ 379 spring.datasource.url=jdbc:postgresql://localhost:5432/medora 380 spring.datasource.username=postgres 381 spring.datasource.password=${DB_PASSWORD} 382 spring.datasource.driver-class-name=org.postgresql.Driver 535 spring.application.name=medora 536 server.port=8081 537 spring.profiles.active=remote 538 spring.config.import=optional:file:.env.properties 539 540 spring.servlet.multipart.max-file-size=5MB 541 spring.servlet.multipart.max-request-size=6MB 542 543 # Spring Security - Development credentials 544 spring.security.user.name=admin 545 spring.security.user.password=${DB_PASSWORD} 546 547 # JWT Configuration 548 jwt.secret=${JWT_SECRET} 549 jwt.expiration=86400000 383 550 384 551 # JPA / Hibernate settings 385 spring.jpa.hibernate.ddl-auto= update552 spring.jpa.hibernate.ddl-auto=none 386 553 spring.jpa.show-sql=true 554 spring.jpa.open-in-view=false 387 555 spring.jpa.properties.hibernate.format_sql=true 388 556 spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect 389 557 390 # HikariCP connection pool ing configuration558 # HikariCP connection pool settings 391 559 spring.datasource.hikari.maximum-pool-size=20 392 560 spring.datasource.hikari.minimum-idle=5 393 spring.datasource.hikari.connection-timeout=30000 394 spring.datasource.hikari.idle-timeout=600000 395 spring.datasource.hikari.max-lifetime=1800000 561 spring.datasource.hikari.connection-timeout=20000 562 spring.datasource.hikari.idle-timeout=300000 563 spring.datasource.hikari.max-lifetime=1200000 564 spring.datasource.hikari.auto-commit=true 396 565 spring.datasource.hikari.leak-detection-threshold=60000 566 567 # Jackson serialization settings 568 spring.jackson.serialization.fail-on-empty-beans=false 569 spring.jackson.default-property-inclusion=non_null 570 397 571 }}} 398 572 … … 404 578 405 579 '''Startup:''' 406 {{{ 407 2026-08-30T13:22:28.551+02:00 INFO 37848 --- [medora] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... 408 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 409 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 410 2026-08-30T13:22:28.675+02:00 INFO 37848 --- [medora] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed. 580 581 {{{ 582 2026-08-30T13:22:28.551+02:00 INFO 37848 --- [medora] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... 583 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 584 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 585 2026-08-30T13:22:28.675+02:00 INFO 37848 --- [medora] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed. 411 586 }}} 412 587 413 588 '''Shutdown:''' 414 {{{ 415 2026-08-30T13:22:31.457+02:00 INFO 37848 --- [medora] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated... 416 2026-08-30T13:22:31.461+02:00 INFO 37848 --- [medora] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed. 417 }}} 418 419 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. 420 589 590 {{{ 591 2026-08-30T13:22:31.457+02:00 INFO 37848 --- [medora] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated... 592 2026-08-30T13:22:31.461+02:00 INFO 37848 --- [medora] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed. 593 }}} 594 595 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. 596 597 This confirms that: 598 599 * HikariCP initializes during application startup. 600 * PostgreSQL connections are created and added to the pool. 601 * Service methods annotated with `@Transactional` obtain connections from the pool while their transactions execute. 602 * Connections are returned to the pool after the transaction completes. 603 * The pool shuts down gracefully when the application stops.
