Skip to main content

Solution Design Baseline — Ghasi EHR Platform

Scope: System-level solution design patterns that ALL modules MUST follow.
Authority: Normative. Module solution designs extend but never contradict this baseline.


1. Layered Architecture (Per Service)

Every NestJS microservice MUST follow this internal layering:

Controller Layer → HTTP/FHIR request handling, DTO validation, auth guards

Application Layer → Use cases / orchestration, event publishing, cross-concern coordination

Domain Layer → Entities, aggregates, value objects, business rules, domain events

Infrastructure → TypeORM repositories, NATS publishers, Redis cache, external adapters

Rules

  • Controllers MUST NOT contain business logic — delegate to application/domain services.
  • Domain entities MUST NOT depend on infrastructure (no TypeORM decorators in domain models if using clean architecture; or colocate if pragmatic but isolate business rules in methods).
  • Application services orchestrate: validate → execute domain logic → persist → publish events → return.

2. DTO & Validation

  • Use class-validator with strict DTOs for all REST endpoints.
  • Return 400 VALIDATION_FAILED with field-level details.
  • FHIR writes validate against base resource rules + declared profile constraints.
  • Validation errors MUST be machine-readable with stable error codes.

3. Authentication & Authorization Flow

Client → Kong (JWT validation) → Service Guard (re-validate JWT, extract tenantId/userId)
→ ModuleEntitlementGuard (check licensing)
→ RolesGuard + @Roles() decorator (RBAC)
→ ABAC check via Access Policy Service (for patient-linked resources)
→ Controller → Service

Critical Rules

  • tenantId and actorId MUST be extracted from JWT claims — NEVER from request body/query params.
  • @Roles() decorators MUST be applied on every endpoint. RolesGuard without @Roles() is a no-op bug.
  • ABAC evaluation via POST /internal/access/evaluate returns { effect, reason, policyId }.
  • Patient portal uses a separate OAuth client/realm with restricted scopes.

4. Event Publishing Pattern

// After successful persistence
await this.repository.save(entity);
await this.natsClient.publish(
CloudEventsBuilder.build({
source: 'ghasi/{service-name}',
type: 'ghasi.{domain}.{entity}.{action}',
tenantid: ctx.tenantId,
actorid: ctx.actorId,
correlationid: ctx.correlationId,
data: { /* minimal event payload */ },
})
);

Rules

  • Publish AFTER successful persistence (not before).
  • Event payloads carry minimal identifiers — consumers fetch full data if needed.
  • Use @ghasi/event-schemas Zod schemas for payload validation.
  • NATS subjects MUST be lowercase dot-delimited.

5. Database Patterns

5.1 Schema Conventions

CREATE TABLE {entity} (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
-- domain columns --
version INTEGER NOT NULL DEFAULT 1, -- optimistic locking
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX idx_{entity}_tenant ON {entity}(tenant_id);

5.2 Rules

  • Migrations only — never synchronize: true.
  • Tenant-scoped indexes on every query path.
  • Soft-delete where retention policy requires; hard-delete only with explicit policy.
  • payload_json (JSONB) for FHIR resource representation alongside indexed columns.

6. FHIR exposure pattern

Each service that owns clinical facts:

  1. Stores data in its own PostgreSQL schema (relational + optional payload_json).
  2. Exposes an internal FHIR adapter interface that fhir-gateway calls.
  3. The adapter translates between internal entities and FHIR R4 JSON.
  4. Gateway handles: routing, tenant isolation, ABAC enforcement, OperationOutcome formatting.
Client → Kong → fhir-gateway → {service}.fhir-adapter → {service} DB

7. Integration / Adapter Pattern

External integrations (HL7 v2, DICOM, X12, national registries):

External System
↕ (MLLP / DICOMweb / HTTPS / SFTP)
Adapter Service (e.g., hl7v2-interop)
→ Parse + validate + deduplicate
→ Map to canonical FHIR
→ Call fhir-gateway or domain service
→ Publish interop.* events
→ Store immutable message log

Rules

  • Adapters MUST preserve external message IDs for deduplication.
  • Adapters MUST NOT introduce competing clinical data models.
  • Retry with exponential backoff; DLQ for failed processing.

8. Error Handling

REST Error Envelope

{
"error": {
"code": "ERROR_CODE",
"message": "Human-readable description",
"details": {}
},
"correlationId": "uuid",
"timestamp": "ISO-8601"
}

Standard Error Codes

CodeHTTPMeaning
VALIDATION_FAILED400DTO/input validation failure
UNAUTHORIZED401Missing/invalid JWT
FORBIDDEN403RBAC/ABAC denied
NOT_FOUND404Resource not found in tenant scope
CONFLICT409Optimistic lock / duplicate
MODULE_NOT_ACTIVE422Module not licensed for this node
RATE_LIMITED429Rate limit exceeded

FHIR Errors

  • Always return OperationOutcome with issue[].code and issue[].diagnostics.

9. Caching Strategy

  • Authorization-aware caching — cache keys include tenant + user/role context.
  • Redis 7 for: entitlement cache (≤30s TTL), FHIR conformance artifacts, search typeaheads.
  • Cache invalidation via NATS events where applicable.

10. Async Processing

Use async workflows for:

  • Integration message ingestion and dispatch
  • Notification delivery (SMS, email, push)
  • PDF/export rendering
  • Batch operations (bulk data export)

Pattern: domain event → NATS → worker consumer → process → emit completion event.

11. Folder Structure Convention (Per Service)

{service-name}/
src/
app.module.ts
{domain}/
{domain}.module.ts
{domain}.controller.ts # REST endpoints
{domain}.service.ts # Application/orchestration
dto/
create-{entity}.dto.ts
update-{entity}.dto.ts
{entity}-response.dto.ts
entities/
{entity}.entity.ts # TypeORM entity
events/
{domain}.events.ts # Event type constants + payloads
guards/ # Module-specific guards if needed
fhir/ # FHIR adapter
{resource}.adapter.ts
common/
filters/
interceptors/
decorators/
config/
test/
unit/
integration/
e2e/
migrations/