The problem
A protocol describes how a study should run. The database has to keep up when that protocol changes. Cadence Clinical connects protocol relationships, day-to-day study records, and an audit history so those pieces can be reasoned about together.
The approach: Neo4j represents protocol relationships; PostgreSQL handles record transactions. Ports and adapters separate the study logic from storage, with append-only audit records and RSA-PSS signatures.
Reported project measurements: Sub-50ms API gateway latency; 100% GxP audit trail traceability; 65% faster cold test runner execution via uv monorepo workspaces.
How it works
Hexagonal Architecture (Ports & Adapters): Domain logic across microservices is strictly decoupled from framework concerns. Ports define explicit interfaces while adapters handle persistence and network integration.
Dual Graph-Relational Data Core: Neo4j graph stores complex Schedule of Activities (SoA) and biomedical concept dependencies; PostgreSQL with SQLModel handles ACID-compliant clinical record transactions.
Cryptographic 21 CFR Part 11 Audit Trails: Every subject data mutation is appended to an in-memory Merkle tree, yielding cryptographic state verification with RSA-PSS digital signatures.
How the pieces connect
flowchart TD
subgraph ProtocolAuthoring [Protocol Designer Service]
A[USDM Protocol Matrix] --> B[Neo4j Graph AST Engine]
B --> C[Amendment Cascading Resolver]
end
subgraph ClinicalCore [Hexagonal Execution Core]
C --> D[Execution Port Interface]
D --> E[PostgreSQL Transactional Engine]
E --> F[Transactional Outbox Relayer]
end
subgraph ComplianceAudit [Cryptographic 21 CFR Part 11]
E --> G[Merkle Tree Audit Append-Only Log]
G --> H[RSA-PSS Digital Signature Generator]
H --> I[Tamper-Proof Audit Digest]
end
Implementation notes
Hexagonal Execution Repository Port (apps/execution/domain/ports.py)
# Hexagonal Repository Port enforcing strict domain isolation
from typing import Protocol, Optional
from datetime import datetime
from pydantic import BaseModel
class ClinicalObservation(BaseModel):
observation_id: str
subject_id: str
tenant_id: str
domain_code: str
value: str
timestamp: datetime
merkle_hash: str
class ExecutionRepositoryPort(Protocol):
"""Enforces persistence-agnostic domain interfaces."""
async def persist_observation(self, observation: ClinicalObservation) -> None: ...
RSA-PSS Digital Signature Verifier (packages/compliance/services/signature.py)
# Cryptographic RSA-PSS 21 CFR Part 11 signature verifier
from cryptography.hazmat.primitives.asymmetric import padding, rsa
from cryptography.hazmat.primitives import hashes
from cryptography.exceptions import InvalidSignature
def verify_manifest_signature(public_key: rsa.RSAPublicKey, signature: bytes, payload: bytes) -> bool:
try:
public_key.verify(
signature, payload,
padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH),
hashes.SHA256()
)
return True
except InvalidSignature:
return False
Tradeoffs and lessons
- RSA-PSS vs. PKCS#1 v1.5: Transitioned all cryptographic signature verification to RSA-PSS to eliminate padding oracle side-channel vulnerabilities.
- Graph-to-Relational Protocol Sync: Offloaded protocol amendment cascading to Neo4j graph traversals, preventing lockouts on high-volume relational subject records during active trials.