Clinical Tool - Modulith
Modular Monolith for a "Clinical Tool" using JPA and Domain Events.
This documentation is a work in progress.
SpringBoot + Java modular monolith for a "Clinical Tool" using JPA for persistence for this Domain Model:
The project is composed by five vertical modules: clinical
, users
, documents
, masterdata
and surveys
.
They are vertical modules in the sense that they do not call each other or database tables from other modules.
Each module exposes a REST API as part of its internal implementation, except clinical
which exposes its functionality through two different API modules: web-api
and mobile-api
.
surveys
module also exposes two different APIs: surveys-backoffice
and surveys-public
but its implementation is an internal implementation detail.
We'll see in the following sections detailed instructions on how to configure ZenWave SDK to generate a modular monolith like this one.
You can find the Complete Source Code at github.
ZenWave Domain Models
This project was generated using the following ZenWave scripts, as always you can run each of these plugins using the ZenWave Model Editor for IntelliJ or from the command line using JBang:
ZenWave Scripts for Clinical Tool - Modulith
config {title "Clinical Tool Backend"basePackage "io.zenwave360.example.clinicaltool"persistence jpadatabaseType postgresqllayout.commonPackage "{{basePackage}}.common"layout.infrastructureRepositoryCommonPackage "{{commonPackage}}"layout.adaptersWebMappersCommonPackage "{{commonPackage}}.mappers"layout.coreImplementationMappersCommonPackage "{{commonPackage}}.mappers"// these should match the values of openapi-generator-maven-pluginopenApiModelNameSuffix DTO// used by ZDLToOpenAPIPluginidType integeridTypeFormat int64// BackendApplicationDefaultPluginuseLombok trueuseSpringModulith truehaltOnFailFormatting falseplugins {ZDLToOpenAPIPlugin {title "Clinical Tool - WebApp API"zdlFiles "models/clinical.zdl"targetFile "src/main/resources/public/apis/webapp-openapi.yml"operationIdsToExclude "getPatientProfileById"}ZDLToOpenAPIPlugin {title "Clinical Tool - Mobile API"zdlFiles "models/clinical.zdl"targetFile "src/main/resources/public/apis/mobile-openapi.yml"operationIdsToInclude "getPatientProfileById,requestOptOut"}ZDLToOpenAPIPlugin {title "Surveys - Backoffice API"zdlFile "models/surveys.zdl"targetFile "src/main/resources/public/apis/surveys-backoffice-openapi.yml"operationIdsToExclude "getSurveyAndQuestionsForPatient,answerSurvey,updateSurveyAnswers,getSurveyAnswers"}ZDLToOpenAPIPlugin {title "Surveys - Public API"zdlFile "models/surveys.zdl"targetFile "src/main/resources/public/apis/surveys-public-openapi.yml"operationIdsToInclude "getSurveyAndQuestionsForPatient,answerSurvey,updateSurveyAnswers,getSurveyAnswers"}ZDLToOpenAPIPlugin {title "Documents API"zdlFile "models/documents.zdl"targetFile "src/main/resources/public/apis/documents-openapi.yml"}ZDLToOpenAPIPlugin {title "Master Data API"zdlFiles "models/masterdata.zdl"targetFile "src/main/resources/public/apis/masterdata-openapi.yml"}ZDLToOpenAPIPlugin {title "Terms And Conditions API"zdlFiles "models/terms-and-conditions.zdl"targetFile "src/main/resources/public/apis/terms-and-conditions-openapi.yml"}ZDLToOpenAPIPlugin {title "User Managament API"zdlFiles "models/users.zdl"openapiOverlayFiles "src/main/resources/public/apis/user-additionalproperties-overlay.yml"targetFile "src/main/resources/public/apis/users-openapi.yml"}//-----------------------------BackendApplicationDefaultPlugin {zdlFile "models/clinical.zdl"// --force // overwite all files}BackendApplicationDefaultPlugin {zdlFile "models/surveys.zdl"// --force // overwite all files}BackendApplicationDefaultPlugin {zdlFile "models/documents.zdl"// --force // overwite all files}BackendApplicationDefaultPlugin {zdlFile "models/masterdata.zdl"// --force // overwite all files}BackendApplicationDefaultPlugin {zdlFile "models/terms-and-conditions.zdl"// --force // overwite all files}BackendApplicationDefaultPlugin {zdlFile "models/users.zdl"// --force // overwite all files}//-----------------------------OpenAPIControllersPlugin {zdlFiles "models/clinical.zdl"openapiFile "src/main/resources/public/apis/webapp-openapi.yml"layout.customWebModule "{{basePackage}}.modules.web.webapp"layout.adaptersWebPackage "{{layout.customWebModule}}"layout.openApiApiPackage "{{layout.customWebModule}}"layout.openApiModelPackage "{{layout.customWebModule}}.dtos"}OpenAPIControllersPlugin {zdlFiles "models/clinical.zdl"openapiFile "src/main/resources/public/apis/mobile-openapi.yml"layout.customWebModule "{{basePackage}}.modules.web.mobile"layout.adaptersWebPackage "{{layout.customWebModule}}"layout.openApiApiPackage "{{layout.customWebModule}}"layout.openApiModelPackage "{{layout.customWebModule}}.dtos"}OpenAPIControllersPlugin {zdlFiles "models/surveys.zdl"openapiFile "src/main/resources/public/apis/surveys-backoffice-openapi.yml"}OpenAPIControllersPlugin {zdlFiles "models/surveys.zdl"openapiFile "src/main/resources/public/apis/surveys-public-openapi.yml"}OpenAPIControllersPlugin {zdlFiles "models/documents.zdl"openapiFile "src/main/resources/public/apis/documents-openapi.yml"}OpenAPIControllersPlugin {zdlFiles "models/masterdata.zdl"openapiFile "src/main/resources/public/apis/masterdata-openapi.yml"}OpenAPIControllersPlugin {zdlFiles "models/terms-and-conditions.zdl"openapiFile "src/main/resources/public/apis/terms-and-conditions-openapi.yml"}OpenAPIControllersPlugin {zdlFiles "models/users.zdl"openapiFile "src/main/resources/public/apis/users-openapi.yml"}//-----------------------------SpringWebTestClientPlugin {zdlFiles "models/clinical.zdl,models/metrics.zdl"openapiFile "src/main/resources/public/apis/webapp-openapi.yml"layout.customWebModule "{{basePackage}}.modules.web.webapp"layout.adaptersWebPackage "{{layout.customWebModule}}"layout.openApiApiPackage "{{layout.customWebModule}}"layout.openApiModelPackage "{{layout.customWebModule}}.dtos"}SpringWebTestClientPlugin {zdlFiles "models/clinical.zdl,models/metrics.zdl"openapiFile "src/main/resources/public/apis/mobile-openapi.yml"layout.customWebModule "{{basePackage}}.modules.web.mobile"layout.adaptersWebPackage "{{layout.customWebModule}}"layout.openApiApiPackage "{{layout.customWebModule}}"layout.openApiModelPackage "{{layout.customWebModule}}.dtos"}SpringWebTestClientPlugin {zdlFiles "models/surveys.zdl"openapiFile "src/main/resources/public/apis/surveys-backoffice-openapi.yml"}SpringWebTestClientPlugin {zdlFiles "models/surveys.zdl"openapiFile "src/main/resources/public/apis/surveys-public-openapi.yml"}SpringWebTestClientPlugin {zdlFiles "models/documents.zdl"openapiFile "src/main/resources/public/apis/documents-openapi.yml"}SpringWebTestClientPlugin {zdlFiles "models/masterdata.zdl"openapiFile "src/main/resources/public/apis/masterdata-openapi.yml"}SpringWebTestClientPlugin {zdlFiles "models/terms-and-conditions.zdl"openapiFile "src/main/resources/public/apis/terms-and-conditions-openapi.yml"}//-----------------------------OpenAPIKaratePlugin {basePackage "{{basePackage}}.adapters.web"testsPackage "karate.webapp"apiFile "src/main/resources/public/apis/webapp-openapi.yml"}OpenAPIKaratePlugin {basePackage "{{basePackage}}.adapters.web"testsPackage "karate.mobile"apiFile "src/main/resources/public/apis/mobile-openapi.yml"}OpenAPIKaratePlugin {basePackage "{{basePackage}}.adapters.web"testsPackage "karate.surveys.backoffice"apiFile "src/main/resources/public/apis/surveys-backoffice-openapi.yml"}OpenAPIKaratePlugin {basePackage "{{basePackage}}.adapters.web"testsPackage "karate.surveys.api"apiFile "src/main/resources/public/apis/surveys-public-openapi.yml"}}}
And ZenWave Domain Models located on folder: https://github.com/ZenWave360/zenwave-playground/tree/main/examples/modulith-clinical-tool-jpa/models
Clinical Tool Domain Model.zdl
config {title "Clinical Tool Backend"layout.moduleBasePackage "io.zenwave360.example.clinicaltool.modules.clinical"// you can choose different layouts, see docs at https://www.zenwave360.io/docs/layout CleanHexagonalProjectLayout}@aggregateentity Hospital {name String required unique maxlength(254)lang String required maxlength(3) /** Primary language of the hospital */timezone String required maxlength(3) /** ECT (Europe/Madrid) */}@aggregate@auditingentity Doctor {userId LongprofilePictureId LonghospitalId Long requiredname String required maxlength(100)surname String required maxlength(100)surname2 String maxlength(100)email String maxlength(100) pattern(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/)phoneNumber String maxlength(20)lang String maxlength(3) /** Primary language of the doctor */}@aggregate@auditingentity Patient {userId LongspikeUUID String /** se modifica directamente via Spike webhook en WearableDataRepository */hospitalId Long requiredprofilePictureId Long@naturalIdphoneNumber String required maxlength(20)@naturalIdhisNumber String required maxlength(100)email String required maxlength(100) pattern(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/)generalInfo GeneralInfo {name String required maxlength(100)surname String required maxlength(100)surname2 String maxlength(100)identityDocumentType IdentityDocumentType requiredidentityDocumentNumber String required maxlength(20)birthDate LocalDate requiredgender GenderType requiredlang String maxlength(3) /** Primary language of the patient */}healthInsuranceInfo HealthInsuranceInfo {insuranceCompanyId String required maxlength(100)insuranceCardNumber String required maxlength(20)}@json documentIds Long[]}relationship OneToMany {Patient{medicalContacts required } to MedicalContact{patient required}Patient{personalContacts required } to PersonalContact{patient required}Patient{patientAddresses required } to PatientAddress{patient required}Patient{hospitalAddresses required } to HospitalAddress{patient required}Patient{patientWearables required } to PatientWearable{patient required}}@aggregateentity ProvisionalPatient {@naturalIdphoneNumber String required maxlength(20)@naturalIdhisNumber String required maxlength(100)@json patient Patient}@skipentity Address {street String required maxlength(100)city String required maxlength(100)postalCode String required maxlength(10)countryCode String required maxlength(3)additionalInfo String maxlength(254)}@copy(Address)entity PatientAddress {current Boolean required}@copy(Address)entity HospitalAddress {}enum IdentityDocumentType {DNI(1), NIE(2), PASSPORT(3)}enum GenderType {MALE(1), FEMALE(2), OTHER(3)}entity PatientWearable {wearableType String requiredserialNumber String maxlength(20)}entity MedicalContact {name String required maxlength(100)surname String required maxlength(100)surname2 String maxlength(100)hospital String maxlength(254)area String maxlength(100)jobPosition String maxlength(100)phoneNumber String required maxlength(20)email String required maxlength(100) pattern(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/)}entity PersonalContact {name String required maxlength(100)surname String required maxlength(100)surname2 String maxlength(100)phoneNumber String required maxlength(20)email String required maxlength(100) pattern(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/)patientRelationshipType PatientRelationshipTypeemergencyContact Boolean required}enum PatientRelationshipType {SON_DAUGTHER(1), CARE_GIVER(2), OTHER(3)}@inlineinput HospitalId {hospitalId Long required}@inlineinput CriterioPatient {hisNumber String required maxlength(100)phoneNumber String required maxlength(20)}output PatientHospital {patientId LonghospitalId LonghisNumber StringfullName Stringgender GenderTypedni StringbirthDate LocalDatephone Stringemail StringinsuranceCardNumber Stringaddress String}@restservice HospitalService for (Hospital, Doctor) {@get("/hospitals/{id}")getHospital(id) Hospital?@post("/hospitals")createHospital(Hospital) Hospital@put("/hospitals/{id}")updateHospital(id, Hospital) Hospital?@get("/hospitals")listHospitals() Hospital[]@post("/doctors")createDoctor(Doctor) Doctor withEvents DoctorCreated@put("/doctors/{id}")updateDoctor(id, Doctor) Doctor?@get("/doctors/{id}")getDoctor(id) Doctor?@get("/doctors")listDoctors() Doctor[]@get("/hospital/{hospitalId}/doctors")@entityForId(Hospital)listHospitalDoctors(HospitalId) Doctor[]@get("/hospital/{hospitalId}/patients")@entityForId(Hospital)listHospitalPatients(HospitalId) PatientHospital[]}input DocumentSignatureRequestedInput {documentInfoId Long requiredpatientId Long required}@restservice PatientsService for (Patient, ProvisionalPatient) {/*** Loads a saved temporal patient or new one if not found.*/@get("/patients/{hisNumber}/{phoneNumber}")loadPatient(@natural id) Patient/*** Persist a temporal patient in the database.*/@patch("/patients/{hisNumber}/{phoneNumber}")partialPatientUpdate(@natural id, Patient) Patient/*** Persite a patient in the database and deletes any temporal data.*/@post("/patients")createPatient(Patient) Patient withEvents PatientCreated/*** Updates a patient in the database.*/@put("/patients/{id}")updatePatient(id, Patient) Patient?/*** Load a patient by id*/@get("/patients/{id}")getPatient(id) Patient?/*** Load a patient profile by id to mobile app.*/@get("/patients/{id}/profile")@policies(isMobile)@entityForId(Patient)getPatientProfileById(id) PatientProfile@post({ path: "/patients/{id}/opt-out", status: 200 })@entityForId(Patient)requestOptOut(id)@listener({model: "models/documents.zdl", event: DocumentSignatureRequested})associateDocumentWithPatient(DocumentSignatureRequestedInput)}output PatientProfile {name String required maxlength(100)surname String required maxlength(100)surname2 String maxlength(100)birthDate LocalDate requiredgender GenderType requiredphoneNumber String required maxlength(20)}event DoctorCreated {doctor Doctor}event PatientCreated {patient Patient}