Procedures App - Architecture Questions

I’m working on the Procedures app for O3 (requirements: Procedure History) and would like feedback on the requirements and the data model before implementation.

Planned Fields

The app will handle both historical procedures (patient intake) and current procedures (documented at facility):

Core fields:

  • Procedure name (coded)
  • Custom procedure name (text) - fallback for unlisted procedures
  • Procedure start datetime
  • Procedure end datetime (null for historical procedures)
  • Actual duration (number)
  • Duration unit (coded: minutes/hours)
  • Outcome (coded)
  • Outcome comment (text, 255 chars)

Form Design

Planning to use a single workspace form with a toggle to switch between “Historical” and “Current” modes. This will adjust field requirements and validation:

  • Historical mode: Minimal required fields (procedure name, approximate date)
  • Current mode: Full detail capture (all time fields, outcome, etc.)

Questions

1. Date Precision for “Historical” Procedures

Patients often only remember approximate dates (“I had surgery in 2018” or “sometime in 2020”). How should we handle this?

Options:

  • Store full date with default values (e.g., 2018-01-01) + add datePrecision field (year/month/day/exact)
  • Custom text field
  • Use separate year/month/day fields
  • Not to collect the date at all
  • Something else?

Any pattern we follow for approximate dates? The only I can think of is the approximate age in the registration.

2. Encounter Linkage

Should procedures link to encounters?

Context:

  • Procedures will use dedicated table (not obs)
  • But form component integration (future feature) would capture procedures during encounter forms

Questions:

  • Should we include encounter_id as an optional field?
  • Do we capture conditions via forms? If so how we handle it? (because conditions doesn’t tie to encounters)

Looking forward to your thoughts!

cc: @ibacher @burke @veronica @dkigen @dkayiwa

4 Likes

You’re right that we don’t have any pattern for approximate dates. Probably the most straight-forward thing to do is to actually store dates as ISO 8601 text strings. This allows representations like “2018”, “2018-01”, “2018-01-01T134123-0530.” This, of course, makes parsing and handling these fields a bit of an issue.

With birthdates, we store a full datetime and a boolean indicating if it’s estimated or not. This is obviously problematic because it’s unclear if it’s estimated “20 years” or “3 years, 8 months” or “sometime in April 2000”. I’m not sure we how we handle this in Conditions, the other domain where this is quite relevant.

As a general rule, they should be FormRecordable (an interface in core) which implies they are tieable to an encounter (just like Conditions). However, this field should be optional.

2 Likes

Thanks for your work on this @jayasanka .

At PIH, we often (especially for surgical procedures) collect pre and post surgery diagnosis related to the procedure. (These diagnoses aren’t recorded as conditions.)

I don’t know if this would be standard for clinicians or data-use, but perhaps this is useful to the discussion.

2 Likes

@jayasanka How about these other fields ~ procedure location, Indication/reason it was done, complications, Procedure Performer/Provider

I like the idea of making this optional because trying to tie a historical procedure to an encounter might be a tall order. But for the current procedures, it’s more likely happening during an encounter and thus linking makes sense.

Good point @ball. KenyaEMR also captures order reason (free text), but I can also see they have a diagnosis field, which I am not quite sure how it’s captured … @dkibet @kmuiruri please chime in

@jayasanka i am very sure that you must have discussed this. But just for the sake of completeness and documentation, what were the considered reasons to create a new dedicated table for procedures instead of using an obs group?

Thanks again for pointing this out @dkayiwa. The implementation can be done either using Obs Groups or a dedicated table. Both approaches come with balanced pros and cons, let me summarize the conversation I had with @ibacher and my observations here:

Option A: Obs Groups

Pros:

  • Leverages existing Obs querying infrastructure
  • Follows established codebase patterns

Cons:

  • More validation code due to the metadata layer
  • Complex querying (must traverse Obs tree with joins)
  • Performance overhead vs flat table
  • Requires ~15 concepts to be created
  • More complex data model (parent Obs + child Obs members)
  • Harder to write direct SQL queries

Option B: Dedicated Table

Pros:

  • Much simpler querying (flat structure, single SELECT)
  • Better performance for large datasets (no joins)
  • Explicit schema, easier to understand, columns clearly defined
  • Direct SQL access for reporting
  • Easy to add indexes on any field
  • Easier to add constraints (NOT NULL, foreign keys)

Cons:

  • Need liquibase migration to create table
  • need to maintain DAO layer, CRUD operations
  • Schema changes require migrations

Obs Groups add about ~30–40% more validation code because of the extra metadata layer needed to build and parse Obs tree structures. On the other hand, validation for a dedicated table is more straightforward, with many rules handled directly at the database level.

This entity is expected to have around 15 fields, and the validation logic also depends on whether the linked procedure is historical or not.

Both options support audit trails and form integration, so those factors don’t really sway the decision. OpenMRS already uses both patterns: for example, conditions and allergies use dedicated tables, while diagnoses rely on Obs Groups.

I lean slightly toward Option B due to the simplicity and performance benefits. However, after evaluating both approaches the difference feels marginal. Either option would be reasonable depending on team preference and long-term maintenance considerations.

@dkayiwa @ibacher @mseaton @burke @mogoodrich @wikumc would love to hear your perspectives.

Thanks so much @veronica @ball on feedback, this is very helpful.

@ibacher re relative times:

another approach would be adding a field called estimatedDateText (String)

It can hold free text like “around 2020 Feb” or “earlier 2019”. We’d still store and treat these as proper dates for sorting and logic, but show this text next to startDateTime in the UI so it’s clearly indicated to the end user that the date is relative.

But I like your approach more, and this pattern is already encouraged by FHIR with partial dates. Datatypes - FHIR v5.0.0

Updated fields:

? = pending decisions

Field Name Type Required (Historical) Required (Current) Description
uuid UUID Yes Yes Unique identifier
patientId UUID Yes Yes Reference to the patient
procedure CodedOrFreeTextAnswer Yes Yes Coded or free-text
bodySite Concept Yes Yes Coded body site
startDateTime Date or Partial Date? Yes Yes When the procedure started; used for sorting
estimatedDateText? String No No Free-text date like “around 2020 Feb” or “earlier 2019”; presence indicates a historical procedure and is displayed next to startDateTime in the UI
endDateTime Date No No When the procedure ended
duration Integer No Yes? Duration of the procedure
durationUnit Enum or Concept or leverage unit column if going with obs No Yes? Unit for duration
encounter Encounter reference No Yes Associated encounter
outcome CodedOrFreeTextAnswer Yes? Yes Coded or free-text outcome
notes String No No Free-text notes

The following fields will be inherited by FormRecordable interface:

  • form_namespace
  • form_field_path

The main weight of this requirement is collecting historical procedures. So to keep the first iteration simple, I suggest we defer decisions on the following fields (even though they’re important) to a next iteration. That way we can get some real usage insights after an initial cycle:

  • indication / reason
  • complications

Was that a typo? Jira

I see… Thanks for pointing that out, Daniel. I was under the wrong impression. Now that I went through the codebase, I see that it was actually moved to a dedicated table from the obs groups a while ago. Any idea what drove that decision? It’s definitely going to be a key input.

@ddesimone @ball @fanderson is this where PIH captures procedures, or could you help me navigate; any specific form I should check out?

I think it’s coming from here.

@mseaton @mogoodrich I’m guessing PIH stores procedures using obs groups. Can you confirm? If so, could you point me to the specific repo and let me know which of the two options you’d prefer?

These guys usually started their life in the emrapi module as obs groups and then eventually moved to core when they became stable. Jira

One other thing that i had not mentioned is that when captured as obs, these guys took advantage of the existing form entry technologies which deal with observations. With a custom table structure, you would need to put some effort into how to collect data for them.

Another point to note is that with obs, you do not require any particular version of openmrs to take advantage of this functionality because all that you are dealing with is specific obs groups that get configured. If you iterate on this new table structure from a module instead of core, then you only require that module.

@jayasanka i said all the above not to sway you in any particular direction, but just for the sake of completeness. :smiley:

1 Like

Thanks @jayasanka for taking this on!

To answer some your questions:

  • At PIH we use obs group (or maybe just obs), mainly because that is the primary, default way to collect clinical data. All the points that @dkayiwa makes are very valid as well–obs are well-supported in form technologies and historically in OpenMRS. If we add procedures as a first-class citizen, we will at some point need to build support in the form technologies, etc, to capture procedures
  • It’s worth noting that at PIH we still collect diagnoses as obs because we haven’t carved out the time to migrate our existing obs group based diagnoses to the first-class diagnoses dedicated table
  • That being said, I do think there is a merit in making Procedures a first-class citizen, as we have done for Conditions and Diagnoses. I think we should lean heavily on what FHIR does… first, the fact that Procedure is a first-class citizen in FHIR is an argument for supporting it in OpenMRS. I would also lean heavily on modelling it the way FHIR does, see ( Procedure - FHIR v5.0.0 ) My general pattern when implementing other domain objects (specifically “Medication Dispense”) was to model as closely to FHIR as possible but don’t worry about modelling every FHIR field, just the ones we know we need in the short-to-mid-term to meet our requirements. (flagging @ibacher in case he disagrees with any of this)
  • We definitely want to link to encounter, optionally (which is what FHIR does)

Take care,

Mark

1 Like

Thanks Daniel and Mark, really helpful context :heart:

Based on what you both shared, my takeaway so far is that both approaches are valid depending on maturity and scope. Another angle is how much the data is expected to change over time: if something is likely to go through revisions, a first-class model tends to be a better fit. If it’s more of a point-in-time capture that rarely gets edited, obs groups make a lot of sense.

That said, if Procedures are something we expect to be queried, reasoned about, and integrated externally, a first class model feels like the right direction to explore. The fact that Procedure is modeled explicitly in FHIR is a meaningful signal there.

Happy to keep iterating on this. the discussion itself is already shaping the design nicely…

@jayasanka is this a feature that you are going to implement in openmrs-core 3.0 and hence make it the required platform version required to access this functionality?

I think we were aiming to integrate it into EMR API in the first instance rather than Core.

I like that approach because it gives you more flexibility to iterate on it and have people try it out without requiring a core platform upgrade. :smiley:

Procedure is relatively mature within FHIR. The required properties include:

  • status - e.g., preparation | in-progress | not-done | on-hold | stopped | completed | entered-in-error | unknown
  • subject - reference to the patient

So, I would recommend including statusstatus_reason) and patient so we’re aligned with FHIR.

Your procedure name (presumably a concept id) aligns with FHIR’s Procedure.code. Ideally, custom name would be required iff the procedure is set to a specific procedure concept like “Procedure, non-coded” … but I couldn’t fiend a CIEL concept for this.

Outcome aligns with FHIR’s Procedure.outcome.

Recording procedure start and optional end time makes sense, but…

  • I don’t understand the end datetime comment “null for historical procedures”. I would expect end datetime to be missing if the procedure is still ongoing or if it wasn’t recorded.
  • Do we need duration? Couldn’t this be determined from start & end times?

Other properties I would include:

  • performer(s) – who performed the procedure. Ideally, would support multiple performers and a function/role for each.
  • note (a text field to document details of the procedure).

I agree with @ibacher. Yes, optionally. Matches FHIR’s Procedure.encounter.

I shared a proposal for better date/time support a couple of years ago that would help with partial dates, including allowing capturing a procedure date like “2018”. Like @ibacher suggested, it uses ISO 8601 dates for the actual data and then allows for a date (or datetime) to hold an interpretation (like 2018-01-01) to be used when an actual date/time is needed. I would love to see us start adopting a convention like this (as long as we are consistent as we adopt it instead of re-inventing a new approach in each case).