1. EMR/HIS Integration Challenges
Most Vietnamese hospitals use internal HIS/EMR systems with traditional relational databases. FHIR integration requires handling many issues.
| Challenge | Description | Solution |
|---|---|---|
| Different Schema | HIS uses its own table, FHIR uses resources | Data mapping layer |
| Data is not standard | Name and address are not in format | Data cleansing + normalization |
| Internal code | HIS uses its own code (ICD-10 VN, hospital drug code) | ConceptMap translation |
| Legacy protocols | HL7 v2, CDA, XML/CSV files | Converter/transformer |
| Performance | HIS serves thousands of users | Caching, async processing |
| Downtime | HIS cannot be stopped | Non-invasive integration |
2. Integration Patterns
Pattern 1: FHIR Facade
Place an FHIR API layer in front of the existing HIS, no change HIS. Facade directly reads/writes the HIS database.
┌─────────────┐ ┌──────────────┐ ┌──────────┐
│ FHIR Client │────▶│ FHIR Facade │────▶│ HIS DB │
│ (App/Portal)│◀────│ (Middleware) │◀────│(Oracle/ │
└─────────────┘ └──────────────┘ │ SQL Srv) │
└──────────┘
@Component
public class HISPatientFacade implements IResourceProvider {
@Autowired
private HISPatientRepository hisRepo;
@Autowired
private PatientMapper mapper;
@Override
public Class<Patient> getResourceType() {
return Patient.class;
}
@Read
public Patient read(@IdParam IdType theId) {
// Đọc từ HIS database
HISBenhNhan bn = hisRepo.findByMaBN(theId.getIdPart());
if (bn == null) {
throw new ResourceNotFoundException(theId);
}
// Map sang FHIR Patient
return mapper.toFhirPatient(bn);
}
@Search
public List<Patient> searchByName(
@RequiredParam(name = Patient.SP_NAME) StringParam name) {
List<HISBenhNhan> results = hisRepo.findByTenContaining(
name.getValue());
return results.stream()
.map(mapper::toFhirPatient)
.collect(Collectors.toList());
}
}
Data Mapper: HIS → FHIR
@Component
public class PatientMapper {
public Patient toFhirPatient(HISBenhNhan bn) {
Patient patient = new Patient();
// ID
patient.setId(bn.getMaBenhNhan());
// Identifier: Mã BN
patient.addIdentifier()
.setSystem("http://bv-abc.vn/fhir/sid/mabn")
.setValue(bn.getMaBenhNhan());
// Identifier: CCCD
if (bn.getSoCCCD() != null) {
patient.addIdentifier()
.setSystem("http://fhir.vn/sid/cccd")
.setValue(bn.getSoCCCD());
}
// Identifier: BHYT
if (bn.getSoBHYT() != null) {
patient.addIdentifier()
.setSystem("http://fhir.vn/sid/bhyt")
.setValue(bn.getSoBHYT());
}
// Họ tên
HumanName name = patient.addName();
name.setFamily(bn.getHo());
name.addGiven(bn.getTen());
name.setText(bn.getHoTen());
// Giới tính: HIS dùng 0/1, FHIR dùng enum
switch (bn.getGioiTinh()) {
case 0: patient.setGender(AdministrativeGender.FEMALE); break;
case 1: patient.setGender(AdministrativeGender.MALE); break;
default: patient.setGender(AdministrativeGender.UNKNOWN);
}
// Ngày sinh
patient.setBirthDate(bn.getNgaySinh());
// Địa chỉ
Address address = patient.addAddress();
address.addLine(bn.getDiaChi());
address.setDistrict(bn.getQuanHuyen());
address.setCity(bn.getTinhThanh());
// Số điện thoại
if (bn.getSoDienThoai() != null) {
patient.addTelecom()
.setSystem(ContactPoint.ContactPointSystem.PHONE)
.setValue(bn.getSoDienThoai());
}
return patient;
}
public HISBenhNhan toHISBenhNhan(Patient patient) {
HISBenhNhan bn = new HISBenhNhan();
// Map ngược từ FHIR → HIS
HumanName name = patient.getNameFirstRep();
bn.setHo(name.getFamily());
bn.setTen(name.getGivenAsSingleString());
bn.setHoTen(name.getText());
// CCCD
patient.getIdentifier().stream()
.filter(i -> "http://fhir.vn/sid/cccd".equals(i.getSystem()))
.findFirst()
.ifPresent(i -> bn.setSoCCCD(i.getValue()));
// Giới tính
switch (patient.getGender()) {
case MALE: bn.setGioiTinh(1); break;
case FEMALE: bn.setGioiTinh(0); break;
default: bn.setGioiTinh(-1);
}
bn.setNgaySinh(patient.getBirthDate());
return bn;
}
}
Pattern 2: Data Sync (ETL)
Synchronize data from HIS to FHIR Server according to schedule (batch) or real-time (CDC).
┌──────────┐ ┌───────────┐ ┌─────────────┐ ┌───────────┐
│ HIS DB │────▶│ ETL/CDC │────▶│ FHIR Server │────▶│ FHIR Apps │
│ │ │ Pipeline │ │ (HAPI FHIR) │ │ │
└──────────┘ └───────────┘ └─────────────┘ └───────────┘
# ETL Pipeline: HIS PostgreSQL → FHIR Server
import psycopg2
import requests
from datetime import datetime
FHIR_BASE = "http://localhost:8080/fhir"
def sync_patients(last_sync: datetime):
"""Đồng bộ bệnh nhân mới/cập nhật từ HIS"""
conn = psycopg2.connect(
host="his-db", dbname="his",
user="readonly", password="***"
)
cur = conn.cursor(cursor_factory=RealDictCursor)
cur.execute("""
SELECT * FROM benh_nhan
WHERE ngay_cap_nhat > %s
ORDER BY ngay_cap_nhat
""", (last_sync,))
for row in cur:
patient = map_to_fhir_patient(row)
# Upsert: PUT với ID
resp = requests.put(
f"{FHIR_BASE}/Patient/{row['ma_bn']}",
json=patient,
headers={"Content-Type": "application/fhir+json"}
)
if resp.status_code in (200, 201):
print(f"Synced Patient/{row['ma_bn']}")
else:
print(f"Error: {resp.status_code} - {resp.text}")
conn.close()
def map_to_fhir_patient(row):
return {
"resourceType": "Patient",
"id": row["ma_bn"],
"identifier": [
{
"system": "http://bv-abc.vn/fhir/sid/mabn",
"value": row["ma_bn"]
}
],
"name": [{
"family": row["ho"],
"given": [row["ten"]],
"text": row["ho_ten"]
}],
"gender": "male" if row["gioi_tinh"] == 1 else "female",
"birthDate": row["ngay_sinh"].isoformat()
}
3. HL7 v2 to FHIR Conversion
Many legacy systems communicate via HL7 v2 messages (ADT, ORM, ORU). FHIR provides transformation mapping.
MSH|^~\&|HIS|BVDK|FHIR|GATEWAY|20250115103000||ADT^A01|MSG001|P|2.5
EVN|A01|20250115103000
PID|1||BN001^^^BVDK||Nguyen^Van A||19900515|M|||123 Nguyen Hue^^HCM^^700000^VN||0901234567
PV1|1|I|KHOA-NOI^P-301^G-01||||BS001^Tran^Thi B
// HL7 v2 → FHIR converter
public class V2ToFhirConverter {
public Bundle convertADT_A01(Message v2Message) {
ADT_A01 adt = (ADT_A01) v2Message;
Bundle bundle = new Bundle();
bundle.setType(Bundle.BundleType.TRANSACTION);
// PID → Patient
PID pid = adt.getPID();
Patient patient = new Patient();
patient.setId(pid.getPatientID().getIDNumber().getValue());
HumanName name = patient.addName();
name.setFamily(pid.getPatientName(0).getFamilyName()
.getSurname().getValue());
name.addGiven(pid.getPatientName(0).getGivenName().getValue());
String gender = pid.getAdministrativeSex().getValue();
patient.setGender("M".equals(gender)
? AdministrativeGender.MALE
: AdministrativeGender.FEMALE);
bundle.addEntry()
.setResource(patient)
.getRequest()
.setMethod(Bundle.HTTPVerb.PUT)
.setUrl("Patient/" + patient.getId());
// PV1 → Encounter
PV1 pv1 = adt.getPV1();
Encounter encounter = new Encounter();
encounter.setStatus(Encounter.EncounterStatus.INPROGRESS);
encounter.setSubject(new Reference(
"Patient/" + patient.getId()));
// Location
String ward = pv1.getAssignedPatientLocation()
.getPointOfCare().getValue();
encounter.addLocation()
.setLocation(new Reference("Location/" + ward));
bundle.addEntry()
.setResource(encounter)
.getRequest()
.setMethod(Bundle.HTTPVerb.POST)
.setUrl("Encounter");
return bundle;
}
}
4. CDA to FHIR Transformation
// CDA Document → FHIR Bundle
public Bundle convertCDAToFhir(ClinicalDocument cda) {
Bundle bundle = new Bundle();
bundle.setType(Bundle.BundleType.DOCUMENT);
// CDA Header → Composition
Composition composition = new Composition();
composition.setStatus(Composition.CompositionStatus.FINAL);
composition.setDate(cda.getEffectiveTime().getValue());
composition.setTitle(cda.getTitle().getText());
// CDA recordTarget → Patient
RecordTarget rt = cda.getRecordTargets().get(0);
Patient patient = mapCDAPatient(
rt.getPatientRole());
composition.setSubject(new Reference(
"Patient/" + patient.getId()));
// CDA Sections → Composition sections
for (Section cdaSection : cda.getComponent()
.getStructuredBody().getComponents()) {
Composition.SectionComponent section =
composition.addSection();
section.setTitle(cdaSection.getTitle().getText());
section.getText().setDiv(
cdaSection.getText().toXml());
// Map CDA entries → FHIR resources
for (Entry entry : cdaSection.getEntries()) {
IBaseResource resource = mapCDAEntry(entry);
bundle.addEntry().setResource(resource);
section.addEntry(new Reference(resource));
}
}
bundle.addEntry().setResource(composition);
bundle.addEntry().setResource(patient);
return bundle;
}
5. Code Translation with ConceptMap
{
"resourceType": "ConceptMap",
"url": "http://bv-abc.vn/fhir/ConceptMap/his-gender-to-fhir",
"name": "HISGenderToFHIR",
"title": "HIS Gender Code → FHIR AdministrativeGender",
"status": "active",
"group": [
{
"source": "http://bv-abc.vn/his/gender",
"target": "http://hl7.org/fhir/administrative-gender",
"element": [
{
"code": "0",
"display": "Nữ",
"target": [
{"code": "female", "relationship": "equivalent"}
]
},
{
"code": "1",
"display": "Nam",
"target": [
{"code": "male", "relationship": "equivalent"}
]
},
{
"code": "2",
"display": "Khác",
"target": [
{"code": "other", "relationship": "equivalent"}
]
}
]
}
]
}
# Dùng $translate operation
curl 'http://localhost:8080/fhir/ConceptMap/$translate?\
system=http://bv-abc.vn/his/gender&\
code=1&\
target=http://hl7.org/fhir/administrative-gender'
6. Middleware Architecture
┌──────────┐ ┌──────────┐ ┌──────────┐
│ HIS 1 │ │ HIS 2 │ │ LIS │
│ (Oracle) │ │(SQL Srv) │ │ (HL7 v2) │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
▼ ▼ ▼
┌────────────────────────────────────────┐
│ Integration Engine │
│ ┌──────────┐ ┌──────────┐ ┌────────┐ │
│ │ Adapters │ │ Mappers │ │ Router │ │
│ │ (DB/v2) │ │ (→FHIR) │ │ │ │
│ └──────────┘ └──────────┘ └────────┘ │
└────────────────┬───────────────────────┘
│
▼
┌──────────────┐
│ FHIR Server │
│ (HAPI FHIR) │
└──────┬───────┘
│
┌──────────┼──────────┐
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│Portal │ │Mobile │ │ HIE │
│BN │ │App │ │Gateway │
└────────┘ └────────┘ └────────┘
7. Integrated Best Practices
- Non-invasive — Do not change existing HIS, use Facade or ETL
- Idempotent — PUT with fixed ID, sync without duplicates
- Incremental sync — Sync change data only (timestamp-based or CDC)
- Error handling — Dead letter queue for failed records
- Monitoring — Track sync lag, error rate, throughput
- Data quality — Validate before pushing to FHIR Server
- ConceptMap — Used for code translation, not hardcode mapping
- Rollback — Keep the original HIS, FHIR as secondary
8. Summary
FHIR Facade — FHIR API layer in front of HIS, realtime map, does not edit HIS
Data Sync (ETL) — Synchronize batch/CDC from HIS DB → FHIR Server
HL7 v2 → FHIR — Convert ADT/ORM/ORU messages to FHIR resources
CDA → FHIR — Transform CDA documents to FHIR Bundle
ConceptMap — Translate HIS internal codes to standard codes (LOINC, SNOMED, ICD-10)
Middleware — Integration engine handles multiple data sources