Core Concepts of O3 Architecture

Let's dive deep into the internals of O3's architecture.

O3 architecture

The major pieces in the O3 frontend architecture are:

  • The app shell - the "base" of the application which coordinates everything.

  • Frontend modules - composable building blocks of the O3 architecture that make up the user interface.

  • The import map - a JSON file that tells the app shell what frontend modules to load and where to load them from.

  • The core framework - a JavaScript library that all frontend modules use.

App shell

In O3, the app shell is the base of the application. It is responsible for doing a whole host of things that are critical to the application's functioning. Introduced in RFC-26, the app shell is responsible for:

  • Setting up the index.html file that gets loaded on startup.

  • Setting up CSS breakpoints in the UI and subscriptions for modals, toasts, and inline notifications.

  • Loading the import map and appending script tags for each frontend module to the index.html file.

  • Loading each frontend module's routes and activator function.

  • Initializing the application's global state store.

  • Setting up the service worker which provides caching capabilities that enable our rudimentary offline mode.

  • Sets up the esm-api library which exports API objects that get reused by multiple frontend modules.

  • Sets up the configuration system which provides various levels of configurability to the application.

  • Sets up the extension system.

Frontend modules

Frontend modules are the basic building blocks of O3. They are also referred to as microfrontends in the O3 architecture (the origins being the similarly named concept in single-spa). Frontend modules get organized into domain-specific monorepos. They get loaded into the application via the import map and export their own default activator and lifecycle functions. These exports conform to a specific microfrontend interface that was introduced in RFC-26.

Before 2023, frontend modules were loaded synchronously, which meant that the application would not render until all frontend modules had been loaded. This was a known performance bottleneck. In 2023 significant work allowed frontend modules to be loaded on-demand, which significantly improved the performance of the application. You can follow the progress here.

Versions of frontend modules explained

Each frontend module is an NPM package with a name ending in -app. OpenMRS ESMs are released with three different tags: "next", "latest", and a version number. Here's what each tag means:

  • 🔴⚠️🚧 "next" = Pre-release, in development. Newest, cutting edge, still under construction. "next" always refers to the most recent but not-yet-released version of an ESM (e.g., 3.2.1-pre.1067). Versions labeled "next" are not recommended for production use as they are considered unstable works-in-progress and have often not undergone integration testing.

  • 🟡 "latest" = Most recent release. "latest" always refers to the most recent released version of an ESM (e.g., 3.2.0). While you can use the "latest" version of any ESM, you have more control by specifying the exact version number of each ESM you use.

  • 🟢✅ vX.X.X = A specific version. A version number always refers to a specific build of an ESM. For example, 3.2.0 or 3.2.1-pre.1067 are both specific versions of the @openmrs/esm-api ESM, though the latter is a pre-release version.

Import map

An import map is a browser specification for controlling the URL to download in-browser JavaScript modules from. Introduced in RFC-4, the import map tells the app shell which frontend modules to use and where to download them from. For example, the import map for the O3 community reference application lives in the OpenMRS distro config repository. Most distros would be expected to follow the same approach.

To view your distro's import map, navigate to /importmap.json, where you'll see something like:

{ "imports": { "@openmrs/esm-home-app": "./openmrs-esm-home-app-4.1.1-pre.211/openmrs-esm-home-app.js", "@openmrs/esm-patient-attachments-app": "./openmrs-esm-patient-attachments-app-4.3.1-pre.1352/openmrs-esm-patient-attachments-app.js", "@openmrs/esm-form-entry-app": "./openmrs-esm-form-entry-app-4.3.1-pre.1352/openmrs-esm-form-entry-app.js", "@openmrs/esm-login-app": "./openmrs-esm-login-app-4.3.2-pre.671/openmrs-esm-login-app.js", "@openmrs/esm-primary-navigation-app": "./openmrs-esm-primary-navigation-app-4.3.2-pre.671/openmrs-esm-primary-navigation-app.js", "@openmrs/esm-patient-banner-app": "./openmrs-esm-patient-banner-app-4.3.1-pre.1352/openmrs-esm-patient-banner-app.js", "@openmrs/esm-offline-tools-app": "./openmrs-esm-offline-tools-app-4.3.2-pre.671/openmrs-esm-offline-tools-app.js", "@openmrs/esm-patient-allergies-app": "./openmrs-esm-patient-allergies-app-4.3.1-pre.1352/openmrs-esm-patient-allergies-app.js", "@openmrs/esm-patient-forms-app": "./openmrs-esm-patient-forms-app-4.3.1-pre.1352/openmrs-esm-patient-forms-app.js", "@openmrs/esm-openconceptlab-app": "./openmrs-esm-openconceptlab-app-3.0.1-pre.24/openmrs-esm-openconceptlab-app.js", "@openmrs/esm-patient-programs-app": "./openmrs-esm-patient-programs-app-4.3.1-pre.1352/openmrs-esm-patient-programs-app.js", "@openmrs/esm-generic-patient-widgets-app": "./openmrs-esm-generic-patient-widgets-app-4.3.1-pre.1352/openmrs-esm-generic-patient-widgets-app.js", "@openmrs/esm-dispensing-app": "./openmrs-esm-dispensing-app-1.0.1-pre.161/openmrs-esm-dispensing-app.js", "@openmrs/esm-devtools-app": "./openmrs-esm-devtools-app-4.3.2-pre.671/openmrs-esm-devtools-app.js", "@openmrs/esm-patient-notes-app": "./openmrs-esm-patient-notes-app-4.3.1-pre.1352/openmrs-esm-patient-notes-app.js", "@openmrs/esm-patient-conditions-app": "./openmrs-esm-patient-conditions-app-4.3.1-pre.1352/openmrs-esm-patient-conditions-app.js", "@openmrs/esm-patient-medications-app": "./openmrs-esm-patient-medications-app-4.3.1-pre.1352/openmrs-esm-patient-medications-app.js", "@openmrs/esm-patient-biometrics-app": "./openmrs-esm-patient-biometrics-app-4.3.1-pre.1352/openmrs-esm-patient-biometrics-app.js", "@openmrs/esm-patient-appointments-app": "./openmrs-esm-patient-appointments-app-4.3.1-pre.1352/openmrs-esm-patient-appointments-app.js", "@openmrs/esm-patient-chart-app": "./openmrs-esm-patient-chart-app-4.3.1-pre.1352/openmrs-esm-patient-chart-app.js", "@openmrs/esm-cohort-builder-app": "./openmrs-esm-cohort-builder-app-3.0.1-pre.115/openmrs-esm-cohort-builder-app.js", "@openmrs/esm-patient-vitals-app": "./openmrs-esm-patient-vitals-app-4.3.1-pre.1352/openmrs-esm-patient-vitals-app.js", "@openmrs/esm-patient-list-app": "./openmrs-esm-patient-list-app-4.3.1-pre.1435/openmrs-esm-patient-list-app.js", "@openmrs/esm-patient-test-results-app": "./openmrs-esm-patient-test-results-app-4.3.1-pre.1352/openmrs-esm-patient-test-results-app.js", "@openmrs/esm-implementer-tools-app": "./openmrs-esm-implementer-tools-app-4.3.2-pre.671/openmrs-esm-implementer-tools-app.js", "@openmrs/esm-active-visits-app": "./openmrs-esm-active-visits-app-4.3.1-pre.1435/openmrs-esm-active-visits-app.js", "@openmrs/esm-outpatient-app": "./openmrs-esm-outpatient-app-4.3.1-pre.1435/openmrs-esm-outpatient-app.js", "@openmrs/esm-appointments-app": "./openmrs-esm-appointments-app-4.3.1-pre.1435/openmrs-esm-appointments-app.js", "@openmrs/esm-patient-registration-app": "./openmrs-esm-patient-registration-app-4.3.1-pre.1435/openmrs-esm-patient-registration-app.js", "@openmrs/esm-fast-data-entry-app": "./openmrs-esm-fast-data-entry-app-1.0.1-pre.74/openmrs-esm-fast-data-entry-app.js", "@openmrs/esm-patient-search-app": "./openmrs-esm-patient-search-app-4.3.1-pre.1435/openmrs-esm-patient-search-app.js", "@openmrs/esm-form-builder-app": "./openmrs-esm-form-builder-app-1.0.3-pre.233/openmrs-esm-form-builder-app.js" } }

The keys of this object are the individual module's names, which are essentially unique identifiers, whereas the values are the module's relative paths to the individual frontend modules. The app shell loads this import map at runtime, and iterates through the list, loading and registering each frontend module into the browser via <script> tags. Presently, O3 utilizes a hybrid module loading approach where:

  • The esm-form-entry-app frontend module, an Angular application that loads the AMPATH form engine into the patient chart, gets loaded via SystemJS.

  • All other frontend modules get loaded via Webpack module federation.

Ideally, it is preferable to have one module loading strategy, and work is in the pipeline to harmonize these approaches.

Core framework

The O3 framework gets packaged as a JavaScript library in the NPM registry. It aggregates all the core libraries for the OpenMRS 3.0 application. These libraries handle concerns such as:

  • Shared utilities

  • Global state management

  • Functions that enable offline mode

  • Global events, variables, and utilities

  • Configuration system

  • API Documentation

  • OpenMRS API functions

  • Breadcrumbs navigation

  • Extension system

  • Global error handling