Patient Intake, Authorization & Billing Platform
Evolving a legacy healthcare workflow into an event-driven, payment-safe modular architecture.
Context
When I joined the project, the patient intake and billing system already existed — but it was tightly coupled, difficult to extend, and fragile under workflow complexity.
Intake, authorization, and billing logic were intertwined in ways that made new feature additions risky. Payment integration lacked strong idempotency guarantees, and failure scenarios were handled reactively rather than systematically.
The goal was not to rewrite from scratch.
The goal was to evolve the system into a production-ready, event-driven architecture that could safely support:
- Patient intake and medical e-forms
- Insurance authorization workflows
- Online payments via Worldpay
- Regulatory logging (HIPAA)
- Upcoming go-live at clinic scale
Architectural Direction
I transitioned the system toward a modular monolith with event-driven boundaries.
Instead of splitting prematurely into microservices, I enforced internal domain separation and event contracts inside a single deployable unit.
Core domains:
- Patient Intake
- Authorization
- Billing
- Payments
- Notifications
Communication between modules occurred via domain events rather than direct method calls.
This allowed loose coupling without distributed system overhead.
System Design
Design principle: isolate payment correctness and authorization integrity from user workflow latency.
Key Architectural Decisions
- Domain events used internally to decouple intake → authorization → billing.
- Payment operations isolated in a dedicated Payment Service.
- Webhook processing treated as first-class event ingestion.
- Immutable audit trail written for every state transition.
Payment Architecture (Worldpay Integration)
Payment correctness was a high-risk area.
Idempotency
- Designed idempotency keys per transaction attempt
- Persisted the request hash and status in PostgreSQL
- Returned the cached outcome on duplicate attempts rather than reprocessing
This prevented double charging during retries or client refreshes.
// payment-idempotency.ts — deterministic key + cached-outcome lookup.
import { createHash } from "node:crypto";
export function paymentIdempotencyKey(input: {
invoiceId: string;
amountCents: number;
patientId: string;
attemptToken: string; // client-supplied; rotates only on a real new attempt
}): string {
const payload = [
input.invoiceId,
input.amountCents,
input.patientId,
input.attemptToken,
].join(":");
return createHash("sha256").update(payload).digest("hex").slice(0, 32);
}
export async function chargeOnce(attempt: PaymentAttempt): Promise<Outcome> {
const key = paymentIdempotencyKey(attempt);
// Single round-trip insert; conflict means a duplicate attempt is in flight
// or already settled. Either way, return the persisted outcome.
const existing = await db.payments.findByIdempotencyKey(key);
if (existing) return existing.outcome;
const outcome = await worldpay.charge(attempt);
await db.payments.persistOutcome(key, outcome);
return outcome;
}
Webhook Reconciliation
Worldpay webhooks were treated as eventual-consistency events:
- Verified every webhook via signature validation
- Updated payment state only after reconciliation against the internal transaction record
- Dropped duplicate webhooks via event fingerprinting
The system assumed:
Webhooks may be delayed, duplicated, or arrive out of order.
And was built accordingly.
Failure Modes
The original system handled failures reactively. I formalized failure handling.
Duplicate authorization requests
- Resolved via deterministic request hashing and request status tracking.
Double billing risk
- Mitigated using idempotent payment keys and transaction locking.
Webhook delay
- Billing state remained in
pending_confirmationuntil verified.
Third-party downtime
- Payment module implemented retry queues with exponential backoff.
- Authorization module decoupled from payment confirmation.
Race conditions between intake and billing
- Event sequencing enforced via domain state machine transitions.
The system now assumes failure as normal — not exceptional.
Compliance & Audit
- All PHI mutations logged.
- Role-based access enforced at module boundary.
- Payment tokens never persisted directly — only reference IDs.
- Audit logs written to S3 with immutable partitioning.
HIPAA and upcoming SOC2 considerations shaped data boundaries early — rather than retrofitting controls later.
Tradeoffs
Why modular monolith instead of microservices?
- Lower operational overhead pre–go-live.
- Easier transactional guarantees.
- Simpler debugging during early clinic rollout.
Microservices can follow when domain boundaries stabilize.
Why event-driven internally?
- Reduced coupling.
- Enabled async processing where needed.
- Prepared system for future service extraction.
Why isolate payments?
Payment correctness and regulatory exposure demanded strict boundaries.
The Payment Service became the most carefully guarded module in the system.
Outcomes
The system is live and serving production traffic across 50+ clinics.
The architecture supports:
- Safe payment retries with idempotency guarantees
- Deterministic authorization workflows
- Event-driven extensibility for new integrations
- Clean domain boundaries between modules
- Regulatory logging built in from day one
Architectural fragility is no longer a deployment risk.
Reflection
What worked
Evolving rather than rewriting reduced delivery risk.
The modular monolith approach provided structure without premature operational complexity.
Treating payments as a high-integrity subsystem was the most important decision.
What I'd improve
Earlier introduction of formal domain event contracts would have reduced refactoring cycles.
Partial payments and refund orchestration remain an area for expansion in future iterations.
Future direction
- Extract Payment Service into independent deployable service post-launch.
- Introduce event streaming (Kafka) once volume justifies it.
- Add formal workflow engine for complex authorization chains.