Terminology Service — User Stories
Service: terminology-service Story prefix: TERM-US Last updated: 2026-04-18 Source: migrated and formatted from
_sources/terminology/backlog/USER_STORIES.md
Stories
TERM-US-001 — Search coded concepts by text
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Full-text concept search returns ranked, tenant-scoped results |
| Epic link | TERM-EPIC-01 |
| Status | Done |
| Priority | Must |
| Story points | 5 |
| Labels | service:terminology-service, type:backend, type:api, slice:S0 |
| Components | concept-query-api |
| FR references | FR-TERM-001 |
| Legacy FR refs | FR-TERM-001 |
| Dependencies | (none) |
User story: As a clinical documentation user, when I search coded terms, I want relevant ranked concept results so that coding is fast and accurate.
Acceptance criteria (Gherkin):
- Given an authenticated tenant user, when a search is run with
limit <= 100, then only active global plus tenant concepts are returned within 200ms p95 under warm-index conditions. - Given an unknown system URI, when search executes, then response is an empty array with HTTP 200.
- Given duplicate term candidates, when response is returned, then deterministic ranking order is preserved for the same input query.
Technical notes:
- PostgreSQL GIN full-text index on
display || ' ' || code. - Tenant scope enforced via RLS +
app.tenant_id.
Definition of Done: Standard DoD plus p95 latency test ≤ 200ms.
TERM-US-002 — Exact concept lookup by code
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Exact system+code lookup returns display and active status |
| Epic link | TERM-EPIC-01 |
| Status | Done |
| Priority | Must |
| Story points | 3 |
| Labels | service:terminology-service, type:backend, type:api, slice:S0 |
| Components | concept-query-api, redis-cache |
| FR references | FR-TERM-002 |
| Legacy FR refs | FR-TERM-002 |
| Dependencies | (none) |
User story: As a service consumer, when I resolve a known code, I want exact code lookup so that downstream displays are consistent.
Acceptance criteria (Gherkin):
- Given active concept exists, when lookup is called with exact
system+code, then response returnssystem/code/displaywith HTTP 200. - Given concept is missing, when lookup is called, then HTTP 404 is returned with
NOT_FOUNDsemantics. - Given inactive concept, when lookup is called, then no active payload is returned.
Technical notes:
- Redis cache key:
term:lookup:{system}:{code}:{tenantId}; TTL 1h.
Definition of Done: Standard DoD plus cache hit/miss integration test green.
TERM-US-003 — Validate a code's active status
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Validation endpoint returns boolean active/valid for a given code |
| Epic link | TERM-EPIC-01 |
| Status | Done |
| Priority | Must |
| Story points | 2 |
| Labels | service:terminology-service, type:backend, type:api, slice:S0 |
| Components | concept-query-api |
| FR references | FR-TERM-003 |
| Legacy FR refs | FR-TERM-003 |
| Dependencies | TERM-US-002 |
User story: As a validation workflow, when I submit a code candidate, I want validity responses so that invalid codes are blocked.
Acceptance criteria (Gherkin):
- Given active code exists, when validate is called, then
result.valid=trueis returned. - Given missing or inactive code, when validate is called, then
result.valid=falseis returned. - Given malformed request missing required fields, when validate is called, then HTTP 400 is returned.
Definition of Done: Standard DoD.
TERM-US-004 — Expand a ValueSet to its active concepts
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | ValueSet $expand returns bounded list of active member concepts |
| Epic link | TERM-EPIC-02 |
| Status | Done |
| Priority | Must |
| Story points | 5 |
| Labels | service:terminology-service, type:backend, type:api, slice:S0 |
| Components | value-set-api, redis-cache |
| FR references | FR-TERM-004 |
| Legacy FR refs | FR-TERM-004 |
| Dependencies | TERM-US-001 |
User story: As an interoperability consumer, when I expand a ValueSet URL, I want a consistent list of active concepts so that decision logic is reproducible.
Acceptance criteria (Gherkin):
- Given mapped ValueSet URL, when expand is called, then active concepts are returned with HTTP 200.
- Given unmapped ValueSet URL, when expand is called, then empty array is returned.
- Given expansion exceeds cap, when response is produced, then returned concepts are limited to configured maximum (500).
Definition of Done: Standard DoD.
TERM-US-005 — Drug class lookup by RxNorm code
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Return drug class(es) for a supplied RxNorm code |
| Epic link | TERM-EPIC-03 |
| Status | Done |
| Priority | Must |
| Story points | 3 |
| Labels | service:terminology-service, type:backend, type:api, slice:S0 |
| Components | cds-api |
| FR references | FR-TERM-005 |
| Legacy FR refs | FR-TERM-005 |
| Dependencies | Drug class data loaded via ETL |
User story: As a CDS engine, when I evaluate medication class risk, I want drug class lookup so that class-based checks are possible.
Acceptance criteria (Gherkin):
- Given known RxNorm code, when drug-class endpoint is called, then one or more class names are returned.
- Given unknown RxNorm code, when endpoint is called, then empty result is returned with HTTP 200.
- Given inactive class rows, when query executes, then inactive rows are excluded.
Definition of Done: Standard DoD.
TERM-US-006 — Drug-drug interaction check
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Return pairwise interactions with severity for a list of RxNorm codes |
| Epic link | TERM-EPIC-03 |
| Status | Done |
| Priority | Must |
| Story points | 5 |
| Labels | service:terminology-service, type:backend, type:api, slice:S0 |
| Components | cds-api |
| FR references | FR-TERM-006 |
| Legacy FR refs | FR-TERM-006 |
| Dependencies | Drug interaction data loaded via ETL |
User story: As a prescribing workflow, when multiple medications are present, I want interaction checks so that unsafe combinations are identified.
Acceptance criteria (Gherkin):
- Given two or more RxNorm codes, when interactions endpoint is called, then matching active interactions are returned with severity and description.
- Given fewer than two codes, when endpoint is called, then HTTP 400 BAD_REQUEST is returned.
- Given no interactions found, when endpoint is called, then empty array is returned.
Definition of Done: Standard DoD plus known CONTRAINDICATED pair verified in integration test.
TERM-US-007 — Duplicate therapy detection
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Identify drug pairs sharing a drug class as potential duplicate therapies |
| Epic link | TERM-EPIC-03 |
| Status | Done |
| Priority | Must |
| Story points | 3 |
| Labels | service:terminology-service, type:backend, type:api, slice:S0 |
| Components | cds-api |
| FR references | FR-TERM-011 |
| Legacy FR refs | FR-TERM-011 |
| Dependencies | TERM-US-005 |
User story: As a clinical safety workflow, when duplicate-therapy screening runs, I want shared-class pair detection so that duplicate therapies are flagged.
Acceptance criteria (Gherkin):
- Given at least two RxNorm codes, when duplicate-therapy endpoint is called, then pairs with
sharedClassesare returned. - Given no shared class exists, when endpoint is called, then empty array is returned.
- Given duplicate input codes only, when endpoint is called, then output excludes same-code pairs.
Definition of Done: Standard DoD.
TERM-US-008 — Drug-condition contraindication check
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Return contraindication alerts for a drug given a patient condition list |
| Epic link | TERM-EPIC-03 |
| Status | Done |
| Priority | Must |
| Story points | 3 |
| Labels | service:terminology-service, type:backend, type:api, slice:S0 |
| Components | cds-api |
| FR references | FR-TERM-012 |
| Legacy FR refs | FR-TERM-012 |
| Dependencies | Drug contraindication data loaded via ETL |
User story: As a prescribing workflow, when patient conditions are known, I want contraindication checks so that drug-condition risks are surfaced.
Acceptance criteria (Gherkin):
- Given
rxnormCodeandicd10Codeslist, when contraindications endpoint is called, then matching alerts include severity and description. - Given no matching contraindications, when endpoint is called, then empty array is returned.
- Given invalid request missing required params, when endpoint is called, then HTTP 400 is returned.
Definition of Done: Standard DoD.
TERM-US-009 — Tenant-scoped visibility enforcement
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Tenant JWT queries return only global + own-tenant concepts |
| Epic link | TERM-EPIC-04 |
| Status | Done |
| Priority | Must |
| Story points | 3 |
| Labels | service:terminology-service, type:backend, slice:S0 |
| Components | concept-query-api, rls-policy |
| FR references | FR-TERM-007 |
| Legacy FR refs | FR-TERM-007 |
| Dependencies | TERM-US-001 |
User story: As a tenant user, when querying terminology, I want tenant-scoped visibility so that cross-tenant leakage is prevented.
Acceptance criteria (Gherkin):
- Given JWT request with
tenantId, when search/lookup executes, then only global plus same-tenant concepts are returned. - Given internal request without
tenantId, when query executes, then only global concepts are returned. - Given request for other-tenant data, when query executes, then data is not returned.
Technical notes: Enforced by PostgreSQL RLS + app.tenant_id session variable set from JWT.
Definition of Done: Standard DoD plus tenant-isolation.spec.ts green.
TERM-US-010 — Secure concept CRUD for administrators
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Platform and tenant admins can create, update, and deactivate concepts per their scope |
| Epic link | TERM-EPIC-04 |
| Status | Done |
| Priority | Must |
| Story points | 5 |
| Labels | service:terminology-service, type:backend, type:api, slice:S0 |
| Components | admin-api |
| FR references | FR-TERM-008 |
| Legacy FR refs | FR-TERM-008 |
| Dependencies | TERM-US-009 |
User story: As an administrator, when governing terminology data, I want secure concept CRUD so that quality and scope are controlled.
Acceptance criteria (Gherkin):
- Given
SUPER_ADMINandtenantId=null, when create concept is requested, then global concept is created. - Given
TENANT_ADMINfor own tenant, when create concept is requested, then tenant concept is created and global create is rejected with 403. - Given duplicate key in same scope, when create is requested, then HTTP 409 CONFLICT is returned.
Definition of Done: Standard DoD.
TERM-US-011 — Bulk CSV concept import
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Platform operator loads licensed terminology data via optional CSV import endpoint |
| Epic link | TERM-EPIC-04 |
| Status | Done |
| Priority | Must |
| Story points | 8 |
| Labels | service:terminology-service, type:backend, slice:S0 |
| Components | import-pipeline |
| FR references | FR-TERM-009 |
| Legacy FR refs | FR-TERM-009 |
| Dependencies | TERM-US-010 |
User story: As a platform operator, when loading licensed data, I want optional CSV import controls so that loading policy is enforceable.
Acceptance criteria (Gherkin):
- Given import is enabled, when valid CSV is posted by authorized actor, then response includes
imported/skipped/errorscounts. - Given import is disabled by env flag, when endpoint is called, then request is rejected consistently.
- Given malformed CSV rows, when import runs, then row-level errors are reported without crashing service.
Definition of Done: Standard DoD.
TERM-US-015 — JWT authentication on public routes
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | All public terminology routes require valid JWT |
| Epic link | TERM-EPIC-05 |
| Status | Done |
| Priority | Must |
| Story points | 2 |
| Labels | service:terminology-service, type:backend, slice:S0 |
| Components | auth-guards |
| FR references | FR-TERM-010 |
| Legacy FR refs | FR-TERM-010 |
| Dependencies | Keycloak realm |
User story: As a security owner, when public APIs are accessed, I want JWT auth and entitlement checks so that unauthorized traffic is blocked.
Acceptance criteria (Gherkin):
- Given missing or invalid JWT, when
/v1or/fhirroutes are called, then HTTP 401 is returned. - Given valid JWT without required entitlement, when route is called, then access is denied.
- Given valid JWT and entitlement, when route is called, then request proceeds.
Definition of Done: Standard DoD.
TERM-US-016 — Internal route shared-secret enforcement
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Internal routes optionally enforce a shared-secret token |
| Epic link | TERM-EPIC-05 |
| Status | Done |
| Priority | Must |
| Story points | 2 |
| Labels | service:terminology-service, type:backend, slice:S0 |
| Components | auth-guards |
| FR references | FR-TERM-010 |
| Legacy FR refs | FR-TERM-010 |
| Dependencies | TERM-US-015 |
User story: As a platform operator, when internal routes are used, I want optional shared-secret enforcement so that trust boundaries remain controlled.
Acceptance criteria (Gherkin):
- Given internal token env unset, when internal endpoint is called, then request is allowed per network policy.
- Given token env set and wrong token provided, when endpoint is called, then HTTP 401 is returned.
- Given token env set and valid token provided, when endpoint is called, then request succeeds.
Definition of Done: Standard DoD.
TERM-US-017 — SLO latency evidence for search endpoint
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Measurable p95 latency evidence produced for NFR compliance |
| Epic link | TERM-EPIC-05 |
| Status | In Progress |
| Priority | Must |
| Story points | 3 |
| Labels | service:terminology-service, type:backend, slice:S0 |
| Components | observability, performance-tests |
| FR references | (NFR) |
| Legacy FR refs | NFR-TERM-001 |
| Dependencies | TERM-US-001 |
User story: As an operations owner, when monitoring terminology latency, I want measurable p95 evidence so that SLO compliance is provable.
Acceptance criteria (Gherkin):
- Given load test execution, when search endpoint is measured, then p95 latency result is recorded against ≤ 200ms target.
- Given dashboard ingestion, when metrics are published, then latency trend is retained for at least 30 days.
- Given threshold breach, when latency exceeds configured limit, then alert rule can be triggered.
Definition of Done: k6 performance test added to CI (scheduled); Grafana dashboard showing latency trend.
TERM-US-018 — Terminology mutation events via NATS
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Concept create, deactivate, and dataset update mutations publish events |
| Epic link | TERM-EPIC-04 |
| Status | Done |
| Priority | Must |
| Story points | 3 |
| Labels | service:terminology-service, type:backend, slice:S0 |
| Components | outbox-relay |
| FR references | (EVENT_MODEL) |
| Legacy FR refs | EVENT_MODEL |
| Dependencies | NATS JetStream |
User story: As an integration consumer, when terminology data changes, I want mutation events so that dependent caches and workflows stay synchronized.
Acceptance criteria (Gherkin):
- Given concept create succeeds, when transaction completes, then
TERMINOLOGY.concept.createdevent is published once. - Given concept deactivate succeeds, when transaction completes, then
TERMINOLOGY.concept.deactivatedevent is published once. - Given import completes, when run finishes, then
TERMINOLOGY.dataset.updatedevent is published with run metadata.
Definition of Done: Standard DoD plus outbox.spec.ts green.
TERM-US-019 — Capacity and licensing-safe import posture
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Service operates within capacity SLOs with licensed data; import posture is compliant |
| Epic link | TERM-EPIC-05 |
| Status | In Progress |
| Priority | Must |
| Story points | 5 |
| Labels | service:terminology-service, type:backend, slice:S0 |
| Components | observability, etl |
| FR references | (NFR) |
| Legacy FR refs | NFR-TERM-002, NFR-TERM-003 |
| Dependencies | TERM-US-017 |
User story: As a platform owner, when scaling terminology operations, I want capacity and licensing-safe import posture so that service growth remains compliant.
Acceptance criteria (Gherkin):
- Given production-like dataset ≥ 300,000 active concepts, when benchmark runs, then query error rate remains within agreed SLO budget.
- Given licensed terminology assets, when imports are performed, then data is loaded without bundling licensed datasets into source repo.
- Given capacity test report generation, when run completes, then measured throughput and resource metrics are stored for audit.
Definition of Done: k6 capacity test passing; .gitignore audit clean; MIGRATION_PLAN.md ETL instructions confirmed.
TERM-ENH-US-001 — TerminologyCapabilities metadata endpoint
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | FHIR $metadata returns TerminologyCapabilities for discovery |
| Epic link | TERM-ENH-EPIC-01 |
| Status | To Do |
| Priority | Should |
| Story points | 3 |
| Labels | service:terminology-service, type:backend, type:api, slice:S1 |
| Components | fhir-operations |
| FR references | FR-TERM-ENH-001 |
| Legacy FR refs | FR-TERM-ENH-001 |
| Dependencies | TERM-EPIC-01 |
User story: As an interoperability consumer, when integrating terminology services, I want TerminologyCapabilities metadata so that supported operations are machine-discoverable.
Acceptance criteria (Gherkin):
- Given
/fhir/R4/metadatais called, when service responds, thenTerminologyCapabilitiesresource includes implemented operations. - Given unsupported operations exist, when metadata is generated, then unsupported capabilities are absent.
Definition of Done: Standard DoD.
TERM-ENH-US-002 — ConceptMap $translate operation
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | FHIR $translate maps codes between systems via ConceptMap |
| Epic link | TERM-ENH-EPIC-01 |
| Status | To Do |
| Priority | Should |
| Story points | 5 |
| Labels | service:terminology-service, type:backend, type:api, slice:S1 |
| Components | concept-map-api |
| FR references | FR-TERM-ENH-002 |
| Legacy FR refs | FR-TERM-ENH-002 |
| Dependencies | TERM-ENH-US-001 |
User story: As an integration workflow, when translating between code systems, I want ConceptMap translation support so that mapping can be automated.
Acceptance criteria (Gherkin):
- Given valid ConceptMap mapping exists, when
$translateis called, then mapped target code is returned with match details. - Given no mapping exists, when request is processed, then no-match response is returned without server error.
Definition of Done: Standard DoD.
TERM-ENH-US-007 — Per-tenant rate limiting
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Per-tenant API quota prevents noisy-tenant impact |
| Epic link | TERM-ENH-EPIC-03 |
| Status | To Do |
| Priority | Could |
| Story points | 5 |
| Labels | service:terminology-service, type:backend, slice:S2 |
| Components | rate-limiting |
| FR references | (NFR) |
| Legacy FR refs | NFR-TERM-ENH-001 |
| Dependencies | TERM-EPIC-05 |
User story: As a platform operator, when tenants consume terminology APIs, I want per-tenant rate limits so that noisy-tenant impact is contained.
Acceptance criteria (Gherkin):
- Given per-tenant quota is configured, when request volume exceeds limit, then excess requests receive HTTP 429.
- Given request is within quota, when endpoint is called, then request succeeds without throttle.
Definition of Done: Standard DoD plus rate-limit integration test green.