Back to Blog
LIMS Development: Building Laboratory Information Management Systems

LIMS Development: Building Laboratory Information Management Systems

December 15, 2024
11 min read
Tushar Agrawal

Complete guide to developing Laboratory Information Management Systems (LIMS). Learn architecture patterns, sample tracking, instrument integration, regulatory compliance, and best practices for clinical and research laboratories.

Introduction

Laboratory Information Management Systems (LIMS) are the digital backbone of modern laboratories. From clinical diagnostics to pharmaceutical research, LIMS manages samples, automates workflows, ensures compliance, and maintains data integrity. Having built LIMS solutions at Dr. Dangs Lab, I'll share comprehensive insights into LIMS development.

What is LIMS?

LIMS is specialized software that manages laboratory operations:

┌─────────────────────────────────────────────────────────────────────┐
│                    LIMS CORE FUNCTIONS                               │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐              │
│  │   Sample     │  │   Workflow   │  │  Instrument  │              │
│  │   Management │  │   Automation │  │  Integration │              │
│  └──────────────┘  └──────────────┘  └──────────────┘              │
│                                                                      │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐              │
│  │   Data       │  │   Quality    │  │   Reporting  │              │
│  │   Management │  │   Control    │  │   & Analytics│              │
│  └──────────────┘  └──────────────┘  └──────────────┘              │
│                                                                      │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐              │
│  │   Inventory  │  │   Regulatory │  │   User       │              │
│  │   Management │  │   Compliance │  │   Management │              │
│  └──────────────┘  └──────────────┘  └──────────────┘              │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

LIMS Architecture

Modern LIMS Architecture

┌─────────────────────────────────────────────────────────────────────┐
│                        PRESENTATION LAYER                            │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐                 │
│  │ Web Portal  │  │ Mobile App  │  │ Desktop App │                 │
│  │ (React/Next)│  │ (React Nat.)│  │ (Electron)  │                 │
│  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘                 │
└─────────┼────────────────┼────────────────┼─────────────────────────┘
          │                │                │
          └────────────────┼────────────────┘
                           │
┌──────────────────────────┼──────────────────────────────────────────┐
│                    API GATEWAY LAYER                                 │
│                 (Authentication, Rate Limiting, Routing)            │
└──────────────────────────┼──────────────────────────────────────────┘
                           │
┌──────────────────────────┼──────────────────────────────────────────┐
│                    MICROSERVICES LAYER                               │
│  ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐       │
│  │ Sample  │ │Workflow │ │ Result  │ │ Report  │ │Inventory│       │
│  │ Service │ │ Service │ │ Service │ │ Service │ │ Service │       │
│  └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘       │
└───────┼───────────┼───────────┼───────────┼───────────┼─────────────┘
        │           │           │           │           │
┌───────┼───────────┼───────────┼───────────┼───────────┼─────────────┐
│                    DATA LAYER                                        │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐                 │
│  │ PostgreSQL  │  │    Redis    │  │Elasticsearch│                 │
│  │  (Primary)  │  │   (Cache)   │  │  (Search)   │                 │
│  └─────────────┘  └─────────────┘  └─────────────┘                 │
└─────────────────────────────────────────────────────────────────────┘
                           │
┌──────────────────────────┼──────────────────────────────────────────┐
│                INTEGRATION LAYER                                     │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐                 │
│  │ HL7 Engine  │  │  ASTM/LIS   │  │ FHIR API    │                 │
│  │             │  │  Interface  │  │             │                 │
│  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘                 │
└─────────┼────────────────┼────────────────┼─────────────────────────┘
          │                │                │
          ▼                ▼                ▼
    ┌──────────┐    ┌──────────┐    ┌──────────┐
    │Lab       │    │Analyzers │    │External  │
    │Equipment │    │          │    │Systems   │
    └──────────┘    └──────────┘    └──────────┘

Database Schema Design

from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Enum, Float, Boolean, Text
from sqlalchemy.orm import relationship, declarative_base
from sqlalchemy.dialects.postgresql import UUID, JSONB
import uuid

Base = declarative_base()

class Sample(Base): __tablename__ = 'samples'

id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) accession_number = Column(String(50), unique=True, nullable=False, index=True) patient_id = Column(UUID(as_uuid=True), ForeignKey('patients.id'), nullable=False) sample_type = Column(String(50), nullable=False) collection_date = Column(DateTime, nullable=False) received_date = Column(DateTime) status = Column(String(50), default='registered') priority = Column(String(20), default='routine') storage_location = Column(String(100)) temperature = Column(Float) volume = Column(Float) container_type = Column(String(50)) collected_by = Column(UUID(as_uuid=True), ForeignKey('users.id')) collection_site = Column(String(100)) notes = Column(Text) metadata = Column(JSONB, default={}) created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, onupdate=datetime.utcnow)

# Relationships patient = relationship('Patient', back_populates='samples') tests = relationship('SampleTest', back_populates='sample') status_history = relationship('SampleStatusHistory', back_populates='sample')

class SampleTest(Base): __tablename__ = 'sample_tests'

id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) sample_id = Column(UUID(as_uuid=True), ForeignKey('samples.id'), nullable=False) test_id = Column(UUID(as_uuid=True), ForeignKey('tests.id'), nullable=False) status = Column(String(50), default='pending') assigned_to = Column(UUID(as_uuid=True), ForeignKey('users.id')) started_at = Column(DateTime) completed_at = Column(DateTime) analyzer_id = Column(UUID(as_uuid=True), ForeignKey('analyzers.id'))

sample = relationship('Sample', back_populates='tests') results = relationship('TestResult', back_populates='sample_test')

class TestResult(Base): __tablename__ = 'test_results'

id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) sample_test_id = Column(UUID(as_uuid=True), ForeignKey('sample_tests.id'), nullable=False) parameter_id = Column(UUID(as_uuid=True), ForeignKey('test_parameters.id'), nullable=False) value = Column(String(100)) numeric_value = Column(Float) unit = Column(String(50)) flag = Column(String(10)) # H, L, HH, LL, C reference_min = Column(Float) reference_max = Column(Float) method = Column(String(100)) performed_by = Column(UUID(as_uuid=True), ForeignKey('users.id')) performed_at = Column(DateTime) validated_by = Column(UUID(as_uuid=True), ForeignKey('users.id')) validated_at = Column(DateTime) comments = Column(Text)

class AuditLog(Base): __tablename__ = 'audit_logs'

id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) table_name = Column(String(100), nullable=False) record_id = Column(UUID(as_uuid=True), nullable=False) action = Column(String(20), nullable=False) # CREATE, UPDATE, DELETE old_values = Column(JSONB) new_values = Column(JSONB) user_id = Column(UUID(as_uuid=True), ForeignKey('users.id')) ip_address = Column(String(45)) user_agent = Column(String(500)) timestamp = Column(DateTime, default=datetime.utcnow)

Core LIMS Features

1. Sample Lifecycle Management

from enum import Enum
from datetime import datetime
from typing import Optional

class SampleLifecycle: """Manages complete sample lifecycle"""

VALID_TRANSITIONS = { 'registered': ['collected', 'cancelled'], 'collected': ['in_transit', 'received'], 'in_transit': ['received', 'lost'], 'received': ['accessioned', 'rejected'], 'accessioned': ['in_process', 'on_hold'], 'in_process': ['completed', 'on_hold', 'failed'], 'on_hold': ['in_process', 'cancelled'], 'completed': ['reported', 'review_required'], 'review_required': ['completed', 'reported'], 'reported': ['archived'], 'rejected': ['recollection_required'], 'failed': ['retest_required', 'cancelled'] }

def __init__(self, db, audit_service, notification_service): self.db = db self.audit = audit_service self.notifications = notification_service

async def transition( self, sample_id: str, new_status: str, user_id: str, reason: Optional[str] = None ) -> dict: """Transition sample to new status""" sample = await self.db.get_sample(sample_id) current_status = sample['status']

# Validate transition if new_status not in self.VALID_TRANSITIONS.get(current_status, []): raise ValueError( f"Invalid transition: {current_status} -> {new_status}" )

# Perform transition await self.db.update_sample(sample_id, {'status': new_status})

# Create audit record await self.audit.log( table='samples', record_id=sample_id, action='STATUS_CHANGE', old_values={'status': current_status}, new_values={'status': new_status}, user_id=user_id, metadata={'reason': reason} )

# Trigger status-specific actions await self._handle_status_actions(sample, new_status, user_id)

return {'sample_id': sample_id, 'status': new_status}

async def _handle_status_actions(self, sample: dict, status: str, user_id: str): """Handle actions triggered by status changes""" actions = { 'received': self._on_received, 'completed': self._on_completed, 'reported': self._on_reported, 'rejected': self._on_rejected }

handler = actions.get(status) if handler: await handler(sample, user_id)

async def _on_completed(self, sample: dict, user_id: str): """Actions when sample testing is complete""" # Check if all tests are complete tests = await self.db.get_sample_tests(sample['id']) all_complete = all(t['status'] == 'completed' for t in tests)

if all_complete: # Auto-trigger report generation await self.notifications.queue_report_generation(sample['id'])

2. Instrument Integration

import asyncio
import serial
from abc import ABC, abstractmethod

class InstrumentInterface(ABC): """Base class for instrument interfaces"""

@abstractmethod async def connect(self): pass

@abstractmethod async def send_worklist(self, samples: list): pass

@abstractmethod async def receive_results(self) -> list: pass

@abstractmethod async def disconnect(self): pass

class ASTMInstrumentInterface(InstrumentInterface): """ASTM E1394 protocol interface for lab analyzers"""

def __init__(self, port: str, baudrate: int = 9600): self.port = port self.baudrate = baudrate self.serial = None

# ASTM control characters STX = b'\x02' # Start of text ETX = b'\x03' # End of text EOT = b'\x04' # End of transmission ENQ = b'\x05' # Enquiry ACK = b'\x06' # Acknowledge NAK = b'\x15' # Negative acknowledge CR = b'\x0D' # Carriage return LF = b'\x0A' # Line feed

async def connect(self): self.serial = serial.Serial( port=self.port, baudrate=self.baudrate, bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, timeout=30 )

async def receive_results(self) -> list: """Receive and parse ASTM results""" results = [] message = b''

while True: byte = self.serial.read(1)

if byte == self.ENQ: # Instrument wants to send self.serial.write(self.ACK)

elif byte == self.STX: # Start of frame message = b''

elif byte == self.ETX: # End of frame self.serial.write(self.ACK) parsed = self._parse_astm_message(message.decode()) if parsed: results.append(parsed) message = b''

elif byte == self.EOT: # End of transmission break

else: message += byte

return results

def _parse_astm_message(self, message: str) -> dict: """Parse ASTM message into structured data""" records = message.split('\r') result = {}

for record in records: if not record: continue

record_type = record[0] fields = record.split('|')

if record_type == 'H': # Header record result['sender'] = fields[4] if len(fields) > 4 else ''

elif record_type == 'P': # Patient record result['patient_id'] = fields[2] if len(fields) > 2 else ''

elif record_type == 'O': # Order record result['sample_id'] = fields[2] if len(fields) > 2 else '' result['test_id'] = fields[4] if len(fields) > 4 else ''

elif record_type == 'R': # Result record result['value'] = fields[3] if len(fields) > 3 else '' result['unit'] = fields[4] if len(fields) > 4 else '' result['flag'] = fields[6] if len(fields) > 6 else ''

return result

class HL7Interface(InstrumentInterface): """HL7 v2.x interface for healthcare systems"""

def __init__(self, host: str, port: int): self.host = host self.port = port self.reader = None self.writer = None

MLLP_START = b'\x0B' # Vertical tab MLLP_END = b'\x1C\x0D' # File separator + CR

async def connect(self): self.reader, self.writer = await asyncio.open_connection( self.host, self.port )

async def send_message(self, message: str): """Send HL7 message wrapped in MLLP""" mllp_message = self.MLLP_START + message.encode() + self.MLLP_END self.writer.write(mllp_message) await self.writer.drain()

# Wait for ACK response = await self.reader.read(1024) return self._parse_ack(response)

def create_oru_message(self, result: dict) -> str: """Create HL7 ORU (Observation Result) message""" timestamp = datetime.now().strftime('%Y%m%d%H%M%S')

segments = [ f"MSH|^~\\&|LIMS|LAB|HIS|HOSPITAL|{timestamp}||ORU^R01|{uuid.uuid4().hex[:8]}|P|2.5", f"PID|1||{result['patient_id']}||{result['patient_name']}", f"OBR|1|{result['order_id']}||{result['test_code']}^{result['test_name']}", f"OBX|1|NM|{result['param_code']}^{result['param_name']}||{result['value']}|{result['unit']}|{result['reference_range']}|{result['flag']}|||F" ]

return '\r'.join(segments)

3. Quality Control Module

import numpy as np
from dataclasses import dataclass
from typing import List, Optional

@dataclass class QCResult: level: str # Level 1, Level 2, Level 3 value: float target_mean: float target_sd: float analyzer_id: str test_code: str lot_number: str expiry_date: datetime performed_by: str performed_at: datetime

class WestgardRules: """Implementation of Westgard QC rules"""

def evaluate(self, results: List[QCResult]) -> dict: """Evaluate QC results against Westgard rules""" if not results: return {'status': 'insufficient_data', 'violations': []}

violations = [] values = [r.value for r in results] mean = results[0].target_mean sd = results[0].target_sd

# Calculate z-scores z_scores = [(v - mean) / sd for v in values]

# 1:3s - Single result > 3SD if abs(z_scores[-1]) > 3: violations.append({ 'rule': '1:3s', 'description': 'Single result exceeds 3 SD', 'severity': 'reject' })

# 1:2s - Single result > 2SD (warning) if abs(z_scores[-1]) > 2: violations.append({ 'rule': '1:2s', 'description': 'Single result exceeds 2 SD', 'severity': 'warning' })

# 2:2s - Two consecutive results > 2SD same side if len(z_scores) >= 2: if (z_scores[-1] > 2 and z_scores[-2] > 2) or \ (z_scores[-1] < -2 and z_scores[-2] < -2): violations.append({ 'rule': '2:2s', 'description': 'Two consecutive results exceed 2 SD on same side', 'severity': 'reject' })

# R:4s - Range of results > 4SD if len(z_scores) >= 2: if abs(z_scores[-1] - z_scores[-2]) > 4: violations.append({ 'rule': 'R:4s', 'description': 'Range between two results exceeds 4 SD', 'severity': 'reject' })

# 4:1s - Four consecutive results > 1SD same side if len(z_scores) >= 4: last_four = z_scores[-4:] if all(z > 1 for z in last_four) or all(z < -1 for z in last_four): violations.append({ 'rule': '4:1s', 'description': 'Four consecutive results exceed 1 SD on same side', 'severity': 'reject' })

# 10x - Ten consecutive results same side of mean if len(z_scores) >= 10: last_ten = z_scores[-10:] if all(z > 0 for z in last_ten) or all(z < 0 for z in last_ten): violations.append({ 'rule': '10x', 'description': 'Ten consecutive results on same side of mean', 'severity': 'reject' })

# Determine overall status has_reject = any(v['severity'] == 'reject' for v in violations) status = 'reject' if has_reject else ('warning' if violations else 'pass')

return { 'status': status, 'violations': violations, 'z_score': z_scores[-1], 'evaluated_at': datetime.utcnow() }

class QCService: def __init__(self, db, notification_service): self.db = db self.notifications = notification_service self.westgard = WestgardRules()

async def record_qc(self, qc_result: QCResult) -> dict: """Record QC result and evaluate""" # Save QC result await self.db.qc_results.insert_one(qc_result.__dict__)

# Get recent QC results for evaluation recent_results = await self.db.qc_results.find({ 'analyzer_id': qc_result.analyzer_id, 'test_code': qc_result.test_code, 'level': qc_result.level }).sort('performed_at', -1).limit(10).to_list(10)

# Evaluate against Westgard rules evaluation = self.westgard.evaluate(recent_results)

# Record evaluation await self.db.qc_evaluations.insert_one({ 'qc_result_id': qc_result.id, evaluation })

# Handle failures if evaluation['status'] == 'reject': await self._handle_qc_failure(qc_result, evaluation)

return evaluation

async def _handle_qc_failure(self, qc_result: QCResult, evaluation: dict): """Handle QC failure - lock analyzer, notify supervisor""" # Lock analyzer await self.db.analyzers.update_one( {'id': qc_result.analyzer_id}, {'$set': {'status': 'qc_locked', 'locked_at': datetime.utcnow()}} )

# Notify supervisor await self.notifications.send_qc_alert( analyzer_id=qc_result.analyzer_id, test_code=qc_result.test_code, violations=evaluation['violations'] )

Compliance Requirements

21 CFR Part 11 Compliance

class ComplianceService:
    """Ensure regulatory compliance"""

async def ensure_electronic_signature( self, user_id: str, document_type: str, document_id: str, meaning: str # "Reviewed", "Approved", "Released" ) -> dict: """21 CFR Part 11 compliant electronic signature"""

# Require re-authentication credentials = await self._request_credentials(user_id)

# Verify credentials if not await self._verify_credentials(user_id, credentials): raise SecurityError("Authentication failed")

# Create signature record signature = { 'id': str(uuid.uuid4()), 'user_id': user_id, 'document_type': document_type, 'document_id': document_id, 'meaning': meaning, 'timestamp': datetime.utcnow(), 'ip_address': self._get_client_ip(), 'signature_hash': self._create_signature_hash( user_id, document_id, meaning ) }

await self.db.electronic_signatures.insert_one(signature)

# Create audit trail entry await self.audit.log( action='ELECTRONIC_SIGNATURE', record_id=document_id, user_id=user_id, metadata={'meaning': meaning} )

return signature

async def ensure_audit_trail(self): """Ensure complete audit trail for all changes""" # Implemented via database triggers and application middleware pass

async def verify_data_integrity(self, record_id: str) -> bool: """Verify data hasn't been tampered with""" record = await self.db.get_record(record_id) stored_hash = record.get('integrity_hash') calculated_hash = self._calculate_hash(record) return stored_hash == calculated_hash

Key Takeaways

1. Compliance First: Build compliance into architecture from day one 2. Instrument Integration: Standard protocols (ASTM, HL7) are essential 3. Audit Everything: Complete audit trails are non-negotiable 4. Quality Control: Implement Westgard rules for analytical quality 5. Data Integrity: Hash verification for tamper detection 6. Scalability: Design for growing sample volumes

Conclusion

LIMS development requires deep understanding of laboratory workflows, regulatory requirements, and healthcare data standards. A well-designed LIMS transforms laboratory operations, improving efficiency, ensuring compliance, and ultimately contributing to better patient care.

---

Building laboratory systems? Connect on LinkedIn to discuss LIMS architecture.

Share this article

Related Articles