Simplifying the pathology LIMS puzzle using Object Oriented Programming 101

I wanted to illustrate how I built a basic system to fill the needs of alternate commercial options that use 100s of developers years to build the perfect solution.

Molecular Pathology deals with the testing of samples at a genomic level, that is, DNA, RNA and/or protein coding. Such testing is far more complex then traditional pathology testing and requires complex workflow management such as nucleic acid extraction, QC, library prep, sequencing & post wet lab analytics (bioinformatics) and reporting with a typical lifecycle taking up to a month.

A Laboratory Information Management System (LIMS) is used to manage and track samples within a laboratory system. There are many commercial products out there catering for traditional pathology but few for the complexities of Molecular Pathology as described above. The primary role of a LIMS is to communicate sample information to and from laboratory instruments, track and process this information, display it in meaningful dashboards and interact with external systems at each end of the sample lifecycle.

After trying many off the shelf products, for our lab, they all presented various shortfalls; chiefly lack of useful dash boarding and huge requirements for custom code writing to integrate with our specific instruments. This is an issue seen by many of the labs around the world I networked with.

A basic LIMS system

The basic requirement is to manage a group of samples (a plate) through each step in the lab.

Many commercial LIMS systems concern themselves with configuration power but if we leave this out each page is essentially just a group of objects (samples) with some input and output manipulation of the data i.e. in/out from an instrument.

So we make one view with one controller with two methods Generate (start step) and Process (finish step) that render some stuff to the view. They key is the engineFactory() method.

def generate() {
    engineFactory()
    model = _processEngine.Generate(plateBarcode, params)
    render(view : "index", model : [message: 
        new MessageGlobal("Viewing Process", 
        EnumStatic.BootstrapAlertTypes.success, false),
        currentProcessModel: CurrentProcess(),
        samples: model, populated: true,
        processHeader: GetLIMSProcess(), viewMode: true]) 
}
def process() {
    engineFactory()
    model = _processEngine.Process(plateBarcode, params)
    render(view : "index", model : [message: 
        new MessageGlobal("Editing Process", 
        EnumStatic.BootstrapAlertTypes.success, false),
        currentProcessModel: CurrentProcess(),
        samples: model, populated: true,
        processHeader: GetLIMSProcess(), viewMode: false]) 
}

Each view is loaded with a “Mode” which is the state or type of step in the lab i.e. DNA extraction, Label etc. The engineFactory() method wires the implementation of each engine to the Process interface and the Generate & Process methods take care of their respective responsibilities.

private void engineFactory(){
    LabProcess process = GetLIMSProcess()
    if (process == LabProcess.QiaSymphony) { 
         //start extraction
        _processEngine = qiaSymphonyEngine
    } else if (process == LabProcess.Qubit) {
        _processEngine = plateReaderEngine
    } else if (process == LabProcess.Dilutions) {
        _processEngine = hamiltonDriverEngine
    } else if (process == LabProcess.Label) {
        _processEngine = labelDriverEngine
    } else if (process == LabProcess.ReceivedManual) {
        _processEngine = receivedManualEngine
    } else if (process == LabProcess.SampleTransfer) {
        _processEngine = transferSampleEngine
    }
}

An example of an engine

public class QiaSymphonyEngine extends Engine implements IProcessEngine {
    public List<LimsSampleModel> Generate(String plateBarcode, 
        Map extraParamsForUpdate){
          //do stuff for this particular step/process/instrument
          //build instrument driver, build model, validate etc
          return qiaSymphonyModel
    }

    public List<LimsSampleModel> Process(String plateBarcode) {
        //do stuff for this particular step/process/instrument
        //process instrument data, build model, validate etc
        return qiaSymphonyModel
    }

Each engine is injected using Spring IOC so all instances are handled externally

//IOC factory
limsSampleEngine(LimsSampleEngine) {
    plateWellSort = ref("plateWellSort")
    sessionFactory = ref("sessionFactory")
}
labelDriverEngine(LabelDriverEngine) {
    limsSampleEngine = ref("limsSampleEngine")
    fileEngine = ref("fileEngine")
    toolBox = ref("toolBox")
    plateWellSort = ref("plateWellSort")
}

qiaSymphonyEngine(QiaSymphonyEngine) {
    limsSampleEngine = ref("limsSampleEngine")
    fileEngine = ref("fileEngine")
    toolBox = ref("toolBox")
    plateWellSort = ref("plateWellSort")
}


.
.
.
.

The other important thing with a clinical system is NATA certification which requires a fool proof audit trail so all engines extend the entity class, as well as implement IProcess interface and all entities (data objects) extend EntityManager

public class LimsSampleEngine extends EntityManager  {
def plateWellSort
def sessionFactory
public class LabelDriverEngine extends Engine implements IProcessEngine {

Entity Manager

public class EntityManager extends Engine{
    public ProcessAudit Save(DateType dateType){
        def audit = new ProcessAudit()
        audit.user = getCurrentUserName().toUpperCase()
        audit.dateTime = new Date()
        audit.dateType = dateType
        audit.ip = getCurrentClientDetails()
        audit.secondCheck = (SecondChecker ?: "").toUpperCase()
        try {
            audit.save()
        } catch (Exception e){
            CsLogger.Error("Base ProcessAudit Save: ${e.message}")
        }
        return audit
    }
.
.
.
.

So every action in every engine and data object calls its appropriate base and records all information to create a NATA & ISO15189 quality trail.

There are lots of other aspects such as models, validation, dashboards, security etc but I wanted to illustrate how easy it was using that 2nd year uni/college OO stuff that is almost a distant memory, lost among the ever changing design pattern landscape.

Key Points

Avoid edge cases

ALter the real life process to meet the software not the other way around

Solve the problem

Don’t create an amazing solution that handles every users’ wim

Focus on dashboards and outward eye candy

By keeping the underlying structure simple but robust we are free to create all sorts of exciting & colorful dashboards and reports which is what the people want.

Focus on auditing

As this is a clinical medical system keep audit engine global but simple and granular

Focus on user experience

There are so many great style frameworks such as angular & bootstrap so focus on great UI’s

Minimize frontend pages

for this to be kept simple we need to keep a “mode based master/slave” type architecture. USE COMPONENT TOOLS SUCH AS REACT TO MINIMIZE REPEATED CODE and pass around ui peices based on the actions being undertaken

This took only a few months to achieve (in my own way) what large software companies have spent years building. Systems that are so generic that you get lost in a sea of configuration options and user defined fields and end up writing your own scripts and software tools to fill the gaps.

Gareth Reid

Leave a comment