A concrete .NET architecture for immutable healthcare audit logs with user attribution, versioning, and regulator-friendly query patterns.
Compliance requirement
In medical software development, audit logging is a core regulatory capability. For clinical laboratory information systems (LIS) seeking ISO 15189 accreditation, the audit log must be complete, tamper-proof, and easily queryable. Regulators require a record of every modification to patient files, diagnostic results, and quality control metrics. If an entry is modified, the system must preserve both the old and new values alongside the identity of the supervisor who authorized the change.
This post details the architecture we designed for a clinical LIS using .NET 8 and PostgreSQL. The system enforces write-once immutability and generates cryptographic hash chains to ensure any administrative tampering is detected instantly.
Event envelope pattern
Every audited action is captured as a structured event. The payload contains the entity type, the database identifier, the user ID, the transaction timestamp, the diff payload, and a SHA-256 integrity hash:
public sealed record AuditEvent(
Guid Id,
string AggregateType,
string AggregateId,
string Action,
string ActorId,
DateTimeOffset OccurredAt,
string PayloadJson,
string Hash
);Write pipeline and integrity validation
To guarantee that audit logs cannot be bypassed, the pipeline uses the Transactional Outbox pattern. Domain updates and audit logs are committed within a single database transaction:
- Domain Write: The application executes a database command (e.g., updating a patient result).
- Outbox Event: The event is written to a local outbox table inside the same transaction.
- Integrity Hash: A background service picks up the event, calculates a HMAC-SHA256 hash combined with the previous record's hash (forming a hash chain), and writes the entry to a read-only audit partition.
- Daily Verification: A nightly job recalculates the hash chain. If a database administrator attempts to modify a log entry, the chain breaks, triggering an alert to the compliance officer.
Query pattern for inspectors
Surveillance audits require extracting the complete history of an entity. We optimize queries using indexes on `aggregate_type` and `aggregate_id` to retrieve data fast:
SELECT aggregate_id, action, actor_id, occurred_at, payload_json
FROM audit_events
WHERE aggregate_type = 'LabResult'
AND occurred_at BETWEEN :from AND :to
ORDER BY occurred_at DESC;Our take
Audit logging cannot be an afterthought implemented via database triggers or basic log files. If compliance logs are not designed as an immutable ledger from day one, preparing for a clinical audit will require weeks of manual data reconstruction. By embedding outbox transactions in your services and securing log entries with cryptographic hash chains, you guarantee system integrity and pass compliance audits with minimal overhead.