Architecture
Overview
Nompd API is a data transformation and distribution layer. It fetches FHIR-structured healthcare data from upstream APIs, converts it into simplified REST models, and exposes it to consumers with versioned diff support.
┌─────────────────────────────────┐
│ Upstream FHIR APIs │
│ (legemidler-api-test) │
│ - PlanDefinition │
│ - ActivityDefinition │
│ - RegulatedAuthorization │
└──────────────┬──────────────────┘
│ POST /api/internal/sync
│ (triggered by K8s CronJob)
▼
┌──────────────────────────────────┐
│ SyncController │
│ └─ SyncService │
│ ├─ UpstreamApiClient │ Fetch raw FHIR data
│ ├─ VersionService │ Increment global version
│ ├─ AppDbContext │ Upsert raw FHIR (versioned)
│ ├─ ConversionService │ Map FHIR → Output models
│ └─ AppDbContext │ Upsert output + write history
└──────────────┬───────────────────┘
│
▼
┌──────────────────────────────────────┐
│ MongoDB (all VersionedDocument<T>) │
│ Raw FHIR: │
│ - PlanDefinitions │
│ - ActivityDefinitions │
│ - RegulatedAuthorizations │
│ Converted output: │
│ - TreatmentGroups │
│ - ReimbursementGroups │
│ Internal: │
│ - SyncMetadata (global counter) │
│ - SyncHistory (one per sync run) │
└──────────────┬───────────────────────┘
│
▼
┌──────────────────────────────────┐
│ Output Controllers │
│ - TreatmentGroupController │
│ - ReimbursementGroupController │
│ │
│ GET /api/v1/treatmentGroup │
│ GET /api/v1/reimbursementGroup │
│ (with optional ?sinceVersion=N) │
└──────────────────────────────────┘
│
▼
API consumers
Directory Structure
Api/
├── Controllers/ API endpoints
│ ├── SyncController POST /api/internal/sync
│ ├── TreatmentGroupController GET /api/v1/treatmentGroup
│ └── ReimbursementGroupController GET /api/v1/reimbursementGroup
├── Data/
│ └── AppDbContext MongoDB collection accessors + indexes
├── Middleware/
│ └── ApiKeyAuthorizationFilter X-API-KEY header validation
├── Models/
│ ├── Fhir/ Upstream FHIR models (deserialized from source APIs)
│ ├── Output/ Converted models (served to consumers)
│ └── Internal/ MongoDB wrapper types (versioning, sync metadata)
├── Services/
│ ├── ConversionService FHIR → Output mapping (pure, no I/O)
│ ├── SyncService Orchestrates fetch → convert → store
│ ├── UpstreamApiClient HTTP client for upstream FHIR APIs
│ └── VersionService Atomic version counter (MongoDB)
├── Program.cs DI registration and startup
├── appsettings.json Base configuration
└── appsettings.Development.json Dev overrides (API keys)
Separation of Concerns
| Layer | Responsibility | I/O |
|---|---|---|
UpstreamApiClient |
Fetch raw FHIR data from external APIs | HTTP |
ConversionService |
Transform FHIR models to output models | None (pure) |
VersionService |
Manage global version counter | MongoDB |
SyncService |
Orchestrate the full sync pipeline | All (coordinates above) |
AppDbContext |
Provide typed MongoDB collection access | MongoDB |
ApiKeyAuthorizationFilter |
Validate API keys on incoming requests | Configuration |
| Output Controllers | Query versioned data and return to clients | MongoDB (read-only) |
Key Design Decisions
Stateless versioning — The server does not track per-client state. Clients are responsible for storing the
currentVersionfrom responses and sending it back via?sinceVersion=N.Soft deletes — When upstream data disappears, records are marked
IsDeleted = truewith the current version number rather than physically removed. This ensures diff consumers see the deletion.Sync via HTTP endpoint — Instead of a
BackgroundService, sync is triggered by a Kubernetes CronJob callingPOST /api/internal/sync. This provides retry policies, logging, and alerting via K8s, and allows manual triggering for debugging.ConversionService is pure — No injected dependencies, no database access. This makes it trivially testable and ensures the mapping logic is isolated from infrastructure concerns.
Generic VersionedDocument
— A single wrapper type handles versioning for any model (raw or converted), avoiding duplication of version/delete tracking logic. Content-hash-based change detection — Each
VersionedDocument<T>stores a SHA256 hash of its BSON content. On sync, only records whose hash actually changed (or whose deletion state flipped) get a new version. This is what makes the version-based diff API meaningful: clients calling?sinceVersion=Nreceive only the records that genuinely changed, not every record after every sync.Raw FHIR persistence — Upstream FHIR resources are stored alongside their converted output, sharing the same version number per sync. This enables re-conversion if mapping logic changes, debugging by comparing raw vs converted, and audit traceability back to the original source.
Sync history — Every sync run produces a
SyncHistoryEntrywith timestamps, durations, per-collection counts, and the business keys of each affected record. This provides durable audit trail and answers "what changed in sync N?" without inspecting the data collections directly.