Architecture overview
The platform is a universal, multi-tenant healthcare/UHP operating foundation — Java 21, Spring Boot 3.5.6 — designed as an Zhenus_UHP-style platform: small deployments activate fewer engines, large ones activate more, but the data model, API shape, and boundaries stay stable.
Engines coupled in core; domain modules decoupled via Feign
coreis the platform. The platform engines —metadata,tenant,facility,module,platform-config,demographic,identity-access,access-control,concept,form,clinical,program,workflow,queue,scheduling,billing,notification— live as packages insidecore(com.zhenus.uhp.api.core.engines.<engine>). They are tightly coupled: they call each other in-process through peer service interfaces and exchange DTOs (never peer repositories or entities — ARCH-001), share one database, and ship as one deployable serving one merged Swagger UI.coreowns the platform's business tables.common(cross-cutting infra: audit/soft-delete base classes, security, crypto, utils) andexchange(public contracts: DTOs + Feign client interfaces) are standalone modules importable by anything.exchangemust never depend oncore.- Future domain modules (e.g.
publichealth,pharmacy,lab) are separate Maven modules that import onlycommon+exchangeand call the platform only via Feign (exchange.client.*) — so they can be extracted to microservices later without changing their callers. Feign is for the domain-module boundary only, never between engines.
Cross-cutting rules every engine follows
-
Modelling discipline — three rules that decide where a value lives.
- Categorical fields are an
enumor a user-managed CRUD entity — never free text. Every "type", "category", "status" or "kind":enumwhen we or a standard fix the values, a CRUD entity (with scope, audit/soft delete, controller and picker) when a tenant, facility or country may need a value we never enumerated. - Clinical vocabularies are concepts; non-clinical vocabularies are the CRUD
types of rule 1. Diagnoses, findings, procedures, lab analytes, allergies and coded form
answers reference a
concept_id;encounter_type,gender,provider_typeand friends stay rule-1 entities. No per-module clinical lookup table, no free-text clinical name column, noenumof diagnoses. The test is the meaning, not the table. - Patient-health data is collected by a drag-and-drop form, not a hand-written
clinical CRUD screen: either a clinical observation form releasing into
Observation, or a drag-and-drop form with a customFormSubmissionHandlerwhen it must also write module tables. The M6 relational form family is retired (FORM-001) — see the form engine page.
Configuration and type CRUD sit outside rules 2 and 3: the test is whether the value is a statement about the patient's health or about how the system is arranged.
- Categorical fields are an
-
Audit & soft delete are foundational. Business entities extend
common.audit.AuditTrail(audit columns +voidedflag + generateduuid) with@SQLRestriction("voided = false")hiding soft-deleted rows; soft delete uses one vocabulary:markAsVoid(String reason). -
UUID primary keys for business/transactional entities (globally unique across distributed/ offline nodes); stable centrally-seeded reference data (country/state/…) may use numeric keys.
-
JSONB discipline: JSONB is for flexible config/schemas/payloads; core relational fields (identifiers, names, dates, status, FKs, concept ids, financial totals) never live only in JSONB.
-
Logical cross-engine references: an engine references another engine's rows by id — no foreign key across engines — so engines stay independently extractable.
-
Object-level access control: patient/person/clinical reads filter by access scope server-side; the unified access decision combines local roles/scopes + identity-access visibility/consent/grants
- feature flags + optional OPA.
-
AFTER_COMMITlisteners must open their own transaction. A@TransactionalEventListener(phase = AFTER_COMMIT)runs while the committed transaction's resources are still bound to the thread, so a write through a plainREQUIREDservice joins a transaction that will never commit again and is silently discarded — the log says success and the database holds nothing. Every such listener carries@Transactional(propagation = REQUIRES_NEW). Discovered at M13-007 (charge capture), then found in both queue listeners by the audit that shipped with!297; mock-based unit tests cannot see this failure, so a listener that writes needs an integration test asserting from a fresh transaction.
Where to go next
- How each feature works — per-engine data model, invariants, and APIs.
- Operations — configuration and feature-flag runbooks.
Authoritative source: core/CORE_PLAN.md and the per-milestone core/MILESTONE*.md notes, being
migrated into these pages.