OpenMRS Architecture
Current-state system documentation — Phase 1 deliverable
Entity Relationship Graph
Click and drag to explore. Scroll to zoom. Click a node for details.
Service Dependency Map
Service layer (purple) to DAO layer (green). Node size = code volume. Dashed lines = cross-service calls.
Request Processing Pipeline
HTTP Request
|
v
CharacterEncodingFilter (UTF-8)
|-> StartupErrorFilter (if startup failed -> error page)
|-> InitializationFilter (if DB not initialized -> setup wizard)
|-> UpdateFilter (if schema upgrade needed -> migration wizard)
|-> MultipartFilter (file uploads)
|-> OpenSessionInViewFilter (Hibernate session per request)
|-> CookieClearingFilter (security)
|-> OpenmrsFilter (attach thread-local UserContext)
|-> CSRFGuard (OWASP CSRF token)
|-> ModuleFilter (route to module filters)
|-> GZIPFilter (response compression)
|-> JspClassLoaderFilter (module classloader)
|
v
Spring DispatcherServlet
|
v
AOP Chain (per @Service method call):
1. AuthorizationAdvice (@Authorized privilege check)
2. LoggingAdvice (TRACE/DEBUG method logging)
3. RequiredDataAdvice (audit field population)
4. CacheInterceptor (@Cacheable/@CacheEvict)
5. TransactionInterceptor (@Transactional)
|
v
Service Layer (21 services, 880 methods)
|
v
DAO Layer (25 DAOs, Criteria API / HQL / Native SQL / Hibernate Search)
|
v
Hibernate ORM
|-> Interceptor Chain:
| AuditableInterceptor (creator/dateCreated/changedBy)
| DropMillisecondsInterceptor (MySQL compat)
| ImmutableObsInterceptor (void-and-replace)
| ImmutableOrderInterceptor (immutable after creation)
|-> Envers (audit trail)
|-> Second-level Cache (Infinispan)
|
v
Database (MySQL / MariaDB / PostgreSQL / H2)
Domain Entity Graph
Patient (extends Person)
|-- identifiers[] --> PatientIdentifier --> PatientIdentifierType
|-- names[] -------> PersonName
|-- addresses[] ---> PersonAddress
|-- attributes[] --> PersonAttribute --> PersonAttributeType
|
|-- encounters[] --> Encounter
| |-- type ---------> EncounterType
| |-- location -----> Location --> LocationTag[]
| |-- providers[] --> EncounterProvider --> Provider, EncounterRole
| |-- obs[] --------> Obs (CASCADE ALL)
| | |-- concept ----> Concept --> ConceptName[], ConceptMap[]
| | |-- valueCoded -> Concept
| | |-- valueDrug --> Drug --> DrugIngredient[]
| | |-- groupMembers[] -> Obs (recursive)
| | |-- order ------> Order
| | '-- referenceRange -> ObsReferenceRange
| |-- orders[] -----> Order (@Inheritance JOINED)
| | |-- DrugOrder (dose, frequency, route, drug)
| | |-- TestOrder
| | |-- ServiceOrder
| | '-- ReferralOrder
| |-- diagnoses[] -> Diagnosis --> CodedOrFreeText
| |-- conditions[] -> Condition --> CodedOrFreeText
| '-- allergies[] -> Allergy --> AllergyReaction[], Allergen
|
|-- visits[] ------> Visit --> VisitType
|-- programs[] ----> PatientProgram --> Program
| |-- states[] -> PatientState --> ProgramWorkflowState
| '-- attributes[] -> PatientProgramAttribute
|-- relationships[] -> Relationship --> RelationshipType
'-- cohorts[] -----> Cohort --> CohortMembership
Concept Dictionary (90 classes, largest subsystem)
Concept (@Cacheable, @Audited)
|-- names[] --------> ConceptName (multi-locale, @Indexed)
|-- descriptions[] -> ConceptDescription
|-- answers[] ------> ConceptAnswer --> Concept (coded values)
|-- conceptSets[] --> ConceptSet (hierarchical grouping)
|-- mappings[] -----> ConceptMap --> ConceptReferenceTerm --> ConceptSource
| '-- ConceptReferenceTermMap (term-to-term: LOINC<->SNOMED)
|-- attributes[] ---> ConceptAttribute
|-- referenceRanges[] -> ConceptReferenceRange
'-- datatype/class -> ConceptDatatype, ConceptClass
85 @Entity classes | 45 data entities | 25 metadata entities | 4 security
Service-to-DAO Dependency Map
Service (lines) DAO (lines) Complexity JPA Migration
-------------------- ------------------ ---------- -------------
ConceptService (2,343) ConceptDAO (2,405) VERY HIGH HARD
OrderService (1,399) OrderDAO (1,027) VERY HIGH HARD
PatientService (1,638) PatientDAO (1,020) VERY HIGH HARD
EncounterService (1,029) EncounterDAO (765) HIGH HARD
ObsService (690) ObsDAO (356) HIGH HARD
PersonService (1,043) PersonDAO (827) MEDIUM-HIGH MEDIUM
UserService (844) UserDAO (809) MEDIUM-HIGH MEDIUM
AdminService (1,120) AdminDAO (450) MEDIUM MEDIUM
FormService (838) FormDAO (704) MEDIUM MEDIUM
ProgramService (683) ProgramDAO (493) MEDIUM MEDIUM
LocationService (541) LocationDAO (432) LOW-MEDIUM EASY
VisitService (450) VisitDAO (395) LOW-MEDIUM EASY
ProviderService (408) ProviderDAO (457) LOW EASY
CohortService (302) CohortDAO (207) LOW EASY
DiagnosisService (290) DiagnosisDAO (242) LOW EASY
+ 6 more simple services...
Total: 21 services (17,627 lines) | 25 DAOs (11,800 lines)
External API Surface
REST Module (/ws/rest/v1/*) FHIR2 Module (/ws/fhir2/*)
163 resources 19 R4 resources
DelegatingCrudResource pattern Translator pattern (HAPI FHIR 5.7.9)
Swagger 2.0 documentation No OpenAPI documentation
HTTP Basic auth HTTP Basic auth
3 representations (REF/DEFAULT/FULL) FHIR standard representations
21 search handlers 18 search parameter classes
Java 21, Jackson 2.19.1 Java 8, HAPI structures R4+DSTU3
Independent modules — same data, different representations
REST serves OpenMRS frontend (O3)
FHIR2 serves interoperability
Security Model
Authentication Flow:
HTTP Basic Header -> AuthorizationFilter -> Context.authenticate()
-> UsernamePasswordAuthenticationScheme
-> HibernateContextDAO.authenticate()
-> Parameterized JPA query (no SQL injection)
-> SHA-512 + per-user salt (128-char) password verification
-> Brute-force lockout after 8 failed attempts
RBAC Model:
User --> Role (Set, recursive inheritance)
--> Privilege (Set, string-based, 250 char max)
Superuser role bypasses ALL privilege checks
@Authorized annotation on service methods
AuthorizationAdvice (AOP) enforces at runtime
No OAuth 2.0 / No SMART on FHIR in core or modules