Core concepts
Architecture
A modular monolith with boundaries that are enforced by tests rather than convention, deployed as a loopback-only container topology. Service extraction is deliberately deferred until it is justified.
#Container topology
The browser reaches an unprivileged Nginx edge, which serves the React client and proxies the API on the same origin. The backend has no published host port. There is no adapter, endpoint, or credential for any prescription network.
Browser
│ HTTP on 127.0.0.1:8081 (the only published application origin)
▼
Nginx edge ── React client + same-origin API proxy
│ internal network only
▼
Spring Boot backend (Java 21, modular monolith)
│ JDBC + same-database relay
▼
PostgreSQL 17 — committed truth + local relay record
╌╌ no adapter or connection ╌╌▶ prescription network / pharmacy
Both the frontend and data Docker networks are declared internal: true, so the backend has no route off the host. That topology — not a lint rule — is what carries the no-egress property.
#Module structure
Every capability module is split into four rings, and the separation is a test rather than a convention:
Capability module (prescribing · review · idempotency · events · audit · terminology)
api controllers, DTOs
└─▶ application orchestration, ports
└─▶ domain framework-free rules
infrastructure ─▶ application JDBC, schedulers, adapters
#Enforced rules
ArchUnit fails the build if:
- the domain gains a Spring, JPA, or Servlet dependency;
- the API layer reaches persistence;
- the application layer imports infrastructure;
- top-level modules form a dependency cycle;
- production code depends on any of six enumerated HTTP-client packages —
java.net.http,javax.net.ssl, Spring'sRestTemplateandWebClient, OkHttp, and Apache HttpClient 5.
The network rule is a denylist, not an exhaustive guarantee — a raw java.net.Socket or HttpURLConnection call would not trip it. The actual no-egress property comes from the container topology described above. Broadening the rule toward deny-by-default is tracked hardening work. Separately, the domain is stricter than its rule requires: every domain source file today imports nothing outside java.* and the project namespace.
#Ports and seams
Extension points are declared as ports in the application or domain layer, with implementations supplied by infrastructure. New capabilities are expected to reuse these rather than invent parallel structures.
| Port | Role | Current implementation |
|---|---|---|
MedicationHydrator | Resolve medication identity from a terminology source | Deterministic synthetic fixture |
QuantityUnitResolver | Resolve versioned quantity units | Database-backed, exactly-one-active enforcement |
DraftPersistence | Commit the clinical aggregate | Single-transaction JDBC implementation |
IdGenerator | Mint prefixed opaque public identifiers | Cryptographically seeded generator |
PrescriptionTransportPort | Future network transport seam | Not implemented |
#The integrity spine
Four mechanisms carry the correctness guarantees, and they are the reason the platform can later be trusted with something more than fixtures.
#Single-transaction clinical commit
The prescription aggregate, medication and diagnosis snapshots, direction lines, validation run, status history, review-grant verifier, audit chain entries, outbox event, and encrypted idempotency record all commit together — or none of them do. A commit guard re-locks the entire authority graph inside the transaction, so an authorization revocation committed mid-flight deterministically wins.
#Hash-chained evidence
Each tenant has an append-only audit stream whose entries are chained by hash, with a locked sequence head and a repeatable-read verifier that re-derives the chain. A parallel stream records security observations. Database triggers reject updates and deletes on evidence rows, so tampering requires breaking the chain visibly.
#Idempotency with exact replay
Keys are scoped by tenant, caller, and operation, then blinded. A serializer-independent semantic fingerprint means logically identical requests match even when their JSON differs. The stored success response is encrypted with AES-256-GCM and bound by associated data, so replay metadata cannot be altered without detection. Retention is bounded, and expired keys become permanent scrubbed tombstones that can never create a second aggregate.
#Database-level lockdown
Persistence spans four migrations across 32 tables. Composite tenant foreign keys cover every tenant-owned edge — 43 established by the tenant-graph migration, extended by exact aggregate bindings later — so a cross-tenant assembly is rejected by PostgreSQL, not merely by application code. The integrity-lockdown migration additionally permits only the two implemented lifecycle state pairs, rejects update and delete on clinical and evidence rows, constrains the replay contract, and forbids outbox deletion.
Those constraints are deliberately restrictive, and new capability is expected to migrate them forward rather than weaken them in place. The status check, the state-pair check, and the transition trigger must evolve in one coordinated change — altering one without the others produces rows that insert but can never transition.
#Technology baseline
| Layer | Baseline |
|---|---|
| Backend | Java 21, Spring Boot 3.5, Spring JDBC, Gradle 8.14 |
| Database | Digest-pinned PostgreSQL 17, in Compose and every integration suite |
| Browser | React 19, TypeScript 5.8, Vite 7, Zod, TanStack Query |
| Edge | Unprivileged Nginx, same-origin proxy, restrictive browser headers |
| Contracts | OpenAPI 3.1, JSON Schema Draft 2020-12 |
| Verification | JUnit, ArchUnit, Testcontainers, Vitest, Testing Library, Playwright, Redocly, AJV, Gitleaks |