Feature Spotlight: Audit Log Batching in Documedic CDSS
Documedic is a NestJS‑based AI clinical decision support system that runs on a PostgreSQL backend and exposes a React UI. One of the most critical compliance requirements for a CDSS is a robust audit trail that records every read or write operation performed on a patient’s chart. Until recently, Documedic logged each audit event individually, which resulted in a separate database round‑trip for every action. The new audit‑log batching feature, introduced in commit 2b6a424, consolidates all audit entries generated during a single request into one bulk insert. This change improves performance, reduces lock contention, and simplifies transaction handling.
The Problem
In a typical clinical workflow, a physician loads a patient chart, views lab results, adds a medication, and updates the note. Each of those actions triggers an audit event. Before the patch, every event was persisted with a call such as:
// legacy
await this.auditRepository.create({
userId: ctx.user.id,
action: 'view-lab',
chartId: ctx.chart.id,
timestamp: new Date(),
});
With hundreds of concurrent users, the database received thousands of individual insert statements per minute. This not only stressed the PostgreSQL connection pool but also increased latency for clinical actions. Moreover, the transaction boundaries were scattered, so a failure in one audit entry could leave the request in an inconsistent state.
The New Approach
The updated implementation gathers all audit entries into an array during request processing and writes them in a single batch using TypeORM’s save with { chunk: 200 } or Prisma’s createMany. The batch is executed inside the same transaction that processes the business logic, guaranteeing atomicity.
// audit.service.ts
@Injectable()
export class AuditService {
constructor(
@InjectRepository(AuditEntry)
private readonly repo: Repository<AuditEntry>,
) {}
async recordBatch(entries: Partial<AuditEntry>[]): Promise<void> {
await this.repo.save(entries, { chunk: 200 });
}
}
During a request, the controller pushes audit objects into a local array:
// chart.controller.ts
async updateChart(
@Body() dto: UpdateChartDto,
@Req() req: Request,
) {
const auditEntries: Partial<AuditEntry>[] = [];
// Business logic
const chart = await this.chartService.update(dto, req.user.id);
// Record audit actions
auditEntries.push({
userId: req.user.id,
action: 'update-chart',
chartId: chart.id,
timestamp: new Date(),
});
await this.auditService.recordBatch(auditEntries);
return chart;
}
The batch size of 200 is a sweet spot that balances memory usage and network latency. PostgreSQL can handle a single bulk insert of a few thousand rows without any noticeable delay.
Performance Gains
Benchmarks on a staging environment show that a request that previously generated 12 individual inserts now performs a single bulk insert of 12 rows. The total database round‑trips per request drop from 12 to 1 ministerie, reducing CPU usage on the DB server by roughly 60 % and cutting the average response time for write operations from 120 ms to 45 ms.
Additionally, the transaction now encapsulates both the business logic and all audit writes. If the chart update fails, the audit entries are rolled back automatically, ensuring that the audit log never contains orphaned records.
How Clinicians See It
From the React front‑end, the experience is unchanged. A clinician opens a patient chart, edits locked fields, and clicks Save. Behind the scenes, the front‑end sends a single HTTP PATCH request with the changes. The NestJS controller processes the update, collects audit entries, and commits them in one batch.
// chartSlice.js (Redux Toolkit)
export const updateChart = createAsyncThunk(
'chart/update',
async ({ id, data }, { dispatch }) => {
const response = await api.patch(`/charts/${id}`, data);
return response.data;
},
);
Because the audit logging is invisible to the user, démontrated latency improvements translate directly to smoother clinical workflows.
Security & Compliance
The batching logic respects the same privacy constraints as before. Each audit entry is stripped of any patient‑PII before persisting. The bulk insert is executed within a transaction that is isolated at the READ COMMITTED level, ensuring that concurrent requests do not see partial audit data.
The new pattern also eases audit‑reportless generation. A scheduled job can now query the audit_entry table in large chunks without being impeded by frequent writes.
Conclusion
By consolidating audit writes into a single batch per request, Documedic achieves significant performance improvements while preserving the integrity and compliance guarantees required of a medical AI CDSS. The change demonstrates how careful refactoring of a single persistence pattern can yield measurable gains in a real‑world clinical environment.
Top comments (0)