Changes between Initial Version and Version 1 of AdvancedApplicationDevelopment


Ignore:
Timestamp:
08/30/26 15:25:16 (11 days ago)
Author:
236021
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • AdvancedApplicationDevelopment

    v1 v1  
     1= Advanced Application Development =
     2
     3== Transactions ==
     4
     5Medora'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
     11Creates a patient and automatically creates their medical record in the same transaction.
     12
     13{{{
     14@Transactional
     15public Patient createPatient(Patient patient) {
     16    if (patient == null || patient.getEmbg() == null || patient.getEmbg().isBlank()) {
     17        throw new IllegalArgumentException("Patient EMBG is required");
     18    }
     19    if (patient.getFirstName() == null || patient.getFirstName().isBlank()) {
     20        throw new IllegalArgumentException("Patient first name is required");
     21    }
     22    if (patient.getLastName() == null || patient.getLastName().isBlank()) {
     23        throw new IllegalArgumentException("Patient last name is required");
     24    }
     25
     26    logger.info("Creating new patient with EMBG: {}", patient.getEmbg());
     27    Patient savedPatient = patientRepository.save(patient);
     28
     29   
     30    try {
     31        MedicalRecord medicalRecord = new MedicalRecord();
     32        medicalRecord.setPatient(savedPatient);
     33        medicalRecordRepository.save(medicalRecord);
     34        logger.info("Created medical record for patient ID: {}", savedPatient.getPatientId());
     35    } catch (Exception e) {
     36        logger.error("Failed to create medical record for patient: {}", e.getMessage());
     37    }
     38
     39    return savedPatient;
     40}
     41}}}
     42
     43'''Transaction behavior:''' both `patientRepository.save()` and `medicalRecordRepository.save()` execute within the same transaction. If either fails, both are rolled back.
     44
     45=== Scenario 2: Auto generate billing from procedures & lab tests ===
     46
     47'''File:''' `src/main/java/medora/service/BillingService.java`
     48
     49Creates a billing record and links every performed procedure and lab test for a patient on a given date.
     50
     51{{{
     52@Transactional
     53public void autoGenerateBillingForPatientService(Long patientId, LocalDate serviceDate) {
     54    try {
     55        if (patientId == null || patientId <= 0) {
     56            throw new IllegalArgumentException("Patient ID must be valid");
     57        }
     58        if (serviceDate == null) {
     59            throw new IllegalArgumentException("Service date must be valid");
     60        }
     61
     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);
     77
     78        logger.info("Found {} procedures and {} lab tests for patient {} on {}",
     79                procedures.size(), labTests.size(), patientId, serviceDate);
     80
     81        if (procedures.isEmpty() && labTests.isEmpty()) {
     82            logger.info("No procedures or lab tests found for patient {} on {}", patientId, serviceDate);
     83            return;
     84        }
     85
     86        BigDecimal procedureCost = procedures.stream()
     87                .map(p -> {
     88                    BigDecimal cost = p.getProcedure().getCost();
     89                    logger.debug("Procedure {} cost: {}", p.getProcedure().getProcedureId(), cost);
     90                    return cost;
     91                })
     92                .reduce(BigDecimal.ZERO, BigDecimal::add);
     93
     94        BigDecimal labTestCost = labTests.stream()
     95                .map(lt -> {
     96                    BigDecimal cost = lt.getLabTest().getCost();
     97                    logger.debug("Lab test {} cost: {}", lt.getLabTest().getTestId(), cost);
     98                    return cost;
     99                })
     100                .reduce(BigDecimal.ZERO, BigDecimal::add);
     101
     102        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
     106        Optional<Admin> adminOptional = adminRepository.findAll()
     107                .stream()
     108                .findFirst();
     109
     110        if (adminOptional.isEmpty()) {
     111            logger.warn("No admin found in system - skipping automatic billing generation for patient {} on {}", patientId, serviceDate);
     112            return;
     113        }
     114
     115        Admin admin = adminOptional.get();
     116
     117        Billing billing = billingRepository.findBillingForPatientOnDate(patientId, serviceDate);
     118
     119        if (billing != null) {
     120            logger.info("Billing record {} already exists for patient {} on {}, updating with new total",
     121                    billing.getBillId(), patientId, serviceDate);
     122            billing.setTotalCost(totalCost);
     123        } else {
     124            billing = new Billing();
     125            billing.setMedicalRecord(medicalRecord);
     126            billing.setAdmin(admin);
     127            billing.setTotalCost(totalCost);
     128            billing.setPaymentStatus(PaymentStatus.PENDING);
     129            billing.setPaymentDate(serviceDate);
     130
     131            logger.info("Creating new billing record for patient {} on {}", patientId, serviceDate);
     132        }
     133
     134        Billing savedBilling = billingRepository.save(billing);
     135        logger.info("Billing record {} for patient {} on {} with total cost: {}",
     136                savedBilling.getBillId(), patientId, serviceDate, totalCost);
     137
     138        for (PerformedProcedures procedure : procedures) {
     139            try {
     140                BillingProcedures billingProcedure = new BillingProcedures(savedBilling, procedure.getProcedure());
     141                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());
     145            }
     146        }
     147
     148        for (PerformedLabTests labTest : labTests) {
     149            try {
     150                BillingLabTests billingLabTest = new BillingLabTests(savedBilling, labTest.getLabTest());
     151                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());
     155            }
     156        }
     157
     158        logger.info("Successfully processed {} procedures and {} lab tests for billing record {}",
     159                procedures.size(), labTests.size(), savedBilling.getBillId());
     160
     161    } catch (Exception e) {
     162        logger.error("Error in auto-billing for patient {} on date {}: {}", patientId, serviceDate, e.getMessage(), e);
     163        throw e;
     164    }
     165}
     166}}}
     167
     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
     170
     171=== Scenario 3: Request a lab test with automated billing ===
     172
     173'''File:''' `src/main/java/medora/service/LabService.java`
     174
     175A doctor requests a lab test for a patient, which automatically triggers billing generation.
     176
     177{{{
     178@Transactional
     179public PerformedLabTests requestLabTestForPatient(Long patientId,
     180                                                  Long doctorId,
     181                                                  Long testId,
     182                                                  LocalDate testDate,
     183                                                  String notes) {
     184    if (patientId == null || patientId <= 0)
     185        throw new IllegalArgumentException("Invalid patient ID");
     186
     187    if (doctorId == null || doctorId <= 0)
     188        throw new IllegalArgumentException("Invalid doctor ID");
     189
     190    if (testId == null || testId <= 0)
     191        throw new IllegalArgumentException("Invalid test ID");
     192
     193    Patient patient = patientRepository.findById(patientId)
     194            .orElseThrow(() -> new RuntimeException("Patient not found"));
     195
     196    Doctors doctor = doctorRepository.findById(doctorId)
     197            .orElseThrow(() -> new RuntimeException("Doctor not found"));
     198
     199    LabTests test = labTestRepository.findById(testId)
     200            .orElseThrow(() -> new RuntimeException("Lab test not found"));
     201
     202    PerformedLabTests performedTest = new PerformedLabTests();
     203    performedTest.setPatient(patient);
     204    performedTest.setDoctor(doctor);
     205    performedTest.setLabTest(test);
     206    LocalDate finalTestDate = testDate != null ? testDate : LocalDate.now();
     207    performedTest.setTestDate(finalTestDate);
     208    performedTest.setNotes(notes);
     209
     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);
     215
     216    return saved;
     217}
     218}}}
     219
     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
     226Stores a lab result and links it to the patient's medical record.
     227
     228{{{
     229@Transactional
     230public 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)
     239        throw new IllegalArgumentException("Invalid test ID");
     240
     241    if (results == null || results.isBlank())
     242        throw new IllegalArgumentException("Results required");
     243
     244    MedicalRecord medicalRecord = medicalRecordRepository.findById(medicalRecordId)
     245            .orElseThrow(() -> new RuntimeException("Medical record not found"));
     246
     247    LabTests labTest = labTestRepository.findById(testId)
     248            .orElseThrow(() -> new RuntimeException("Lab test not found"));
     249
     250    LabResults labResult = new LabResults();
     251    labResult.setResults(results);
     252    labResult.setResultDate(resultDate);
     253    labResult.setLabTest(labTest);
     254
     255    LabResults saved = labResultsRepository.save(labResult);
     256
     257    // Use the join repository to safely link to avoid deleting existing links
     258    // Create and save the join entity explicitly
     259
     260    MedicalRecordLabResults link = new MedicalRecordLabResults();
     261    link.setMedicalRecord(medicalRecord);
     262    link.setLabResult(saved);
     263
     264    logger.info("Stored lab result {} for medical record {}", saved.getResultId(), medicalRecordId);
     265    return medicalRecordLabResultRepository.save(link);
     266}
     267}}}
     268
     269'''Transaction behavior:''' Both the lab result creation and the link to the medical record happen atomically. If either fails, both are rolled back.
     270
     271=== Scenario 5: Appointment creation ===
     272
     273'''File:''' `src/main/java/medora/service/AppointmentService.java`
     274
     275Creates an appointment with validation for future dates, no double-booking, and no duplicate appointments.
     276
     277{{{
     278@Transactional
     279public Appointment createAppointment(Long patientId,
     280                                     Long doctorId,
     281                                     LocalDate appointmentDate,
     282                                     LocalTime appointmentTime) {
     283
     284    if (patientId == null || patientId <= 0) {
     285        throw new IllegalArgumentException("Patient ID must be valid");
     286    }
     287
     288    if (doctorId == null || doctorId <= 0) {
     289        throw new IllegalArgumentException("Doctor ID must be valid");
     290    }
     291
     292    if (appointmentDate == null) {
     293        throw new IllegalArgumentException("Appointment date is required");
     294    }
     295
     296    if (appointmentTime == null) {
     297        throw new IllegalArgumentException("Appointment time is required");
     298    }
     299
     300    Patient patient = patientRepository.findById(patientId)
     301            .orElseThrow(() ->
     302                    new RuntimeException("Patient not found with ID: " + patientId));
     303
     304    Doctors doctor = doctorRepository.findById(doctorId)
     305            .orElseThrow(() ->
     306                    new RuntimeException("Doctor not found with ID: " + doctorId));
     307
     308    LocalDateTime appointmentDateTime =
     309            LocalDateTime.of(appointmentDate, appointmentTime);
     310
     311    // Future validation
     312
     313    if (!appointmentDateTime.isAfter(LocalDateTime.now())) {
     314        throw new RuntimeException(
     315                "Appointment must be scheduled for a future date and time"
     316        );
     317    }
     318
     319    // Doctor slot validation
     320    boolean doctorBusy =
     321            appointmentRepository
     322                    .existsByDoctorDoctorIdAndAppointmentDateAndAppointmentTimeAndStatusNot(
     323                            doctorId,
     324                            appointmentDate,
     325                            appointmentTime,
     326                            AppointmentStatus.CANCELLED
     327                    );
     328
     329    if (doctorBusy) {
     330        throw new RuntimeException("This appointment slot is already booked");
     331    }
     332
     333    // Duplicate patient validation
     334    boolean duplicateAppointment =
     335            appointmentRepository
     336                    .existsByPatientPatientIdAndDoctorDoctorIdAndAppointmentDateAndAppointmentTimeAndStatusNot(
     337                            patientId,
     338                            doctorId,
     339                            appointmentDate,
     340                            appointmentTime,
     341                            AppointmentStatus.CANCELLED
     342                    );
     343
     344    if (duplicateAppointment) {
     345        throw new RuntimeException(
     346                "Patient already has this appointment scheduled"
     347        );
     348    }
     349
     350    Appointment appointment = new Appointment();
     351
     352    appointment.setPatient(patient);
     353    appointment.setDoctor(doctor);
     354    appointment.setAppointmentDate(appointmentDate);
     355    appointment.setAppointmentTime(appointmentTime);
     356    appointment.setStatus(AppointmentStatus.SCHEDULED);
     357
     358    logger.info(
     359            "Creating appointment for patient ID: {} with doctor ID: {}",
     360            patientId,
     361            doctorId
     362    );
     363
     364    return appointmentRepository.save(appointment);
     365}
     366}}}
     367
     368'''Transaction behavior:''' Validation queries and appointment creation all happen within a single transaction.
     369
     370----
     371
     372== Database Connection Pooling ==
     373
     374=== Current configuration ===
     375
     376`src/main/resources/application.properties` defines the datasource, plus the explicit HikariCP pool settings:
     377
     378{{{
     379spring.datasource.url=jdbc:postgresql://localhost:5432/medora
     380spring.datasource.username=postgres
     381spring.datasource.password=${DB_PASSWORD}
     382spring.datasource.driver-class-name=org.postgresql.Driver
     383
     384# JPA / Hibernate settings
     385spring.jpa.hibernate.ddl-auto=update
     386spring.jpa.show-sql=true
     387spring.jpa.properties.hibernate.format_sql=true
     388spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
     389
     390# HikariCP connection pooling configuration
     391spring.datasource.hikari.maximum-pool-size=20
     392spring.datasource.hikari.minimum-idle=5
     393spring.datasource.hikari.connection-timeout=30000
     394spring.datasource.hikari.idle-timeout=600000
     395spring.datasource.hikari.max-lifetime=1800000
     396spring.datasource.hikari.leak-detection-threshold=60000
     397}}}
     398
     399Spring 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.
     400
     401=== Connection pool in use ===
     402
     403Real startup and shutdown log output from the running application:
     404
     405'''Startup:'''
     406{{{
     4072026-08-30T13:22:28.551+02:00  INFO 37848 --- [medora] [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
     4082026-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
     4092026-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
     4102026-08-30T13:22:28.675+02:00  INFO 37848 --- [medora] [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
     411}}}
     412
     413'''Shutdown:'''
     414{{{
     4152026-08-30T13:22:31.457+02:00  INFO 37848 --- [medora] [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
     4162026-08-30T13:22:31.461+02:00  INFO 37848 --- [medora] [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.
     417}}}
     418
     419This 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