Insurance Claims System Design
Design a scalable enterprise Insurance Claims System — covering first notice of loss, multi-stage adjudication workflow, document management, fraud detection integration, reserve management, settlement and payment, regulatory compliance, and end-to-end audit trail.
1-Hour Interview Roadmap
| Time | Topic |
|---|---|
| 0 – 5 min | Requirements clarification |
| 5 – 10 min | Capacity estimation |
| 10 – 18 min | High-level architecture + core services |
| 18 – 28 min | Claim lifecycle — FNOL through settlement |
| 28 – 36 min | Adjudication workflow engine + business rules |
| 36 – 43 min | Document management + evidence handling |
| 43 – 50 min | Fraud detection integration + reserve management |
| 50 – 56 min | Database design + API design |
| 56 – 60 min | Trade-offs + common interview mistakes |
What Are We Building?
An enterprise Insurance Claims System (ICS) that manages the complete lifecycle of an insurance claim — from the moment a policyholder reports an incident (First Notice of Loss) through investigation, adjudication, reserve setting, settlement approval, and payment disbursement.
An insurance claim is not a simple transaction. It is a multi-week, multi-party workflow involving policyholders, claims adjusters, medical/auto/property assessors, legal reviewers, fraud investigators, and payment approvers — all operating under strict regulatory deadlines and compliance obligations.
Scale reference: State Farm processes ~30,000 claims per day. Allstate processes ~15,000 claims per day. A mid-tier insurer processes 2,000–10,000 claims per day. Design for 10,000 claims per day across property, auto, and health lines of business.
Key unique challenges:
- Long-running workflows — a claim can take days to months to resolve; state must be durable and recoverable after any failure
- Multi-party coordination — adjusters, assessors, medical reviewers, and legal teams each own different stages; handoffs must be tracked
- Regulatory compliance — every state/jurisdiction has mandatory acknowledgment and settlement deadlines (e.g., 15 days to acknowledge, 30 days to accept/deny); breaches incur fines
- Fraud detection — 10–15% of claims have fraud indicators; detection must not delay legitimate claims while flagging suspicious ones
- Immutable audit trail — every action, decision, and communication on a claim must be permanently recorded for regulatory examination and litigation
Step 1 — Requirements
Functional Requirements
| # | Requirement |
|---|---|
| 1 | Policyholder can file a First Notice of Loss (FNOL) via web, mobile, phone, or agent portal |
| 2 | System creates a claim record and assigns a unique claim number immediately |
| 3 | System routes the claim to the appropriate adjuster queue based on line of business, severity, and geography |
| 4 | Adjuster can review claim details, request additional documents, and update claim status |
| 5 | System supports multi-stage adjudication: intake → investigation → evaluation → decision → settlement |
| 6 | Policyholder and assessors can upload supporting documents (photos, repair estimates, medical bills) |
| 7 | System sets and tracks financial reserves (estimated claim cost) per claim |
| 8 | System integrates with fraud detection service; flags suspicious claims for investigation |
| 9 | Approved claims trigger payment disbursement to policyholder or third-party |
| 10 | System enforces regulatory deadlines and alerts adjusters when SLA windows are approaching |
| 11 | Supervisor can review and override adjuster decisions on high-value or disputed claims |
| 12 | Full audit trail of every action, decision, and communication on every claim |
Non-Functional Requirements
| # | Requirement |
|---|---|
| 1 | FNOL submission must respond within 3 seconds (p99) — policyholder is in distress |
| 2 | Zero claim loss — no claim can be silently dropped; all failures must be retried or surfaced |
| 3 | Full immutable audit trail — every state change, decision, and document upload permanently logged |
| 4 | Regulatory deadline enforcement — system must alert and escalate when deadlines are at risk |
| 5 | High availability — 99.9% uptime for FNOL and claim status APIs |
| 6 | Document storage — support files up to 100MB per document; retain for 7 years minimum |
| 7 | HIPAA compliance for health insurance claims (PII/PHI encryption at rest and in transit) |
| 8 | Role-based access control — adjusters see only their assigned claims; supervisors see their team |
Out of Scope
- Policy underwriting and premium calculation
- Policy issuance and renewal management
- Reinsurance treaty management
- Agent commission management
- Customer self-service portal UI (assume API consumers build UI)
Step 2 — Capacity Estimation
Traffic Estimates
Daily claims filed (FNOL): 10,000
Average rate: 10,000 / 86,400 = ~0.12 FNOL/sec
Peak rate (disaster event): 50× normal = ~6 FNOL/sec (CAT event — hurricane, earthquake)
Workflow actions per claim: ~30 actions over claim lifetime (uploads, decisions, comms)
Daily workflow actions: 10,000 × 30 = 300,000 actions/day = ~3.5 actions/sec
Document uploads per claim: ~8 documents avg (photos, estimates, records)
Daily uploads: 10,000 × 8 = 80,000 uploads/day
Avg document size: 5 MB
Daily document storage: 80,000 × 5 MB = ~400 GB/day
Claim status reads:
Policyholder checks × 5 per day per active claim
Active claims at any time: ~150,000 (avg 15-day lifecycle)
Daily reads: 150,000 × 5 = 750,000 reads/day = ~9 QPS avg
Storage Estimates
Per claim record:
Claim header: ~1 KB
Workflow events (30 avg): ~200 bytes each = 6 KB
Reserve history: ~500 bytes × 5 entries = 2.5 KB
Total per claim (metadata): ~10 KB
Annual metadata storage:
10,000 claims/day × 10 KB × 365 = ~36.5 GB/year (trivial)
Document storage:
400 GB/day × 365 = ~146 TB/year (requires object storage — S3 / Azure Blob)
7-year retention: 146 TB × 7 = ~1 PB total
Payment records:
10,000 settlements/day × 500 bytes = ~5 MB/day
Key Insights
- Metadata is tiny — 36.5 GB/year; fits in a well-indexed relational DB
- Documents dominate storage — 1 PB over 7 years; requires cloud object storage with lifecycle policies
- Normal load is modest — 3.5 actions/sec is low; CAT events create 50× spikes; queue-based architecture absorbs bursts
- Long claim lifetimes — 150,000 claims active at any time; workflow state must be durable, not in-memory
- Compliance is a hard requirement — 7-year document retention, deadline tracking, and audit log are not optional
Step 3 — High-Level Architecture
flowchart TD
Channels["FNOL Channels\nWeb / Mobile / Phone / Agent"]
AG["API Gateway\nAuth + Rate Limiting + TLS"]
CS["Claims Service\n(Orchestrator + state machine)"]
WE["Workflow Engine\n(Task routing + deadline tracking)"]
DS["Document Service\n(Upload + retrieval + virus scan)"]
FS["Fraud Service\n(Scoring + investigation queue)"]
RS["Reserve Service\n(Financial reserve management)"]
PS["Payment Service\n(Settlement disbursement)"]
NS["Notification Service\n(Policyholder + adjuster alerts)"]
AuditS["Audit Service\n(Append-only event log)"]
ClaimDB[("PostgreSQL\nClaims DB")]
DocStore[("S3 / Object Storage\nDocument Store")]
KF["Kafka\nClaim Events Bus"]
REDIS["Redis\nDeadline Cache + Session"]
Channels --> AG
AG --> CS
CS --> WE
CS --> DS
CS --> RS
CS --> ClaimDB
CS --> KF
CS --> REDIS
KF --> FS
KF --> NS
KF --> AuditS
KF --> PS
DS --> DocStore
FS --> ClaimDB
RS --> ClaimDB
PS --> ClaimDB
AuditS --> ClaimDB
Component Responsibilities
| Component | Responsibility |
|---|---|
| Claims Service | Central orchestrator — creates claims, drives state machine, coordinates all downstream services |
| Workflow Engine | Routes tasks to adjuster queues; tracks deadlines; escalates approaching SLA breaches |
| Document Service | Handles file uploads, virus scanning, metadata extraction, and secure retrieval with pre-signed URLs |
| Fraud Service | Scores claims for fraud indicators; maintains investigation queue for flagged claims |
| Reserve Service | Sets and adjusts financial reserves; tracks reserve history; reports to finance |
| Payment Service | Processes approved settlement payments; integrates with ACH/wire/check payment rails |
| Notification Service | Alerts policyholders and adjusters via email, SMS, and portal messages |
| Audit Service | Subscribes to all claim events; writes append-only audit records; never updates or deletes |
| Claims DB | PostgreSQL — all claim metadata, workflow state, reserves, payments (source of truth) |
| Document Store | S3 / Azure Blob — all uploaded files; 7-year retention; WORM (write-once, read-many) for compliance |
| Kafka | Event bus — decouples Claims Service from Fraud, Notifications, Audit, and Payments |
| Redis | Deadline window cache; adjuster session state; claim lock (prevent concurrent edits) |
Step 4 — Claim Lifecycle: FNOL to Settlement
The complete end-to-end journey of a claim from first report through payment.
sequenceDiagram
participant P as Policyholder
participant AG as API Gateway
participant CS as Claims Service
participant WE as Workflow Engine
participant FS as Fraud Service
participant KF as Kafka
participant DB as Claims DB
P->>AG: POST /v1/claims/fnol {policy_number, incident_date, type, description}
AG->>CS: Forward authenticated request
CS->>DB: Validate policy is active + in-force
CS->>DB: INSERT claim {status: FNOL_RECEIVED, claim_number: auto}
CS->>DB: INSERT claim_event {FNOL_SUBMITTED, actor: policyholder}
CS->>KF: Publish claim.fnol_received {claim_id, type, severity_hint}
CS->>WE: CreateWorkflow(claim_id, line_of_business, severity)
WE->>DB: INSERT workflow_tasks {ACKNOWLEDGE_CLAIM, due: NOW() + 15 days}
WE->>DB: INSERT workflow_tasks {ASSIGN_ADJUSTER, due: NOW() + 2 days}
CS-->>P: 201 {claim_number, status: "received", next_steps}
Note over KF,FS: Async — fraud scoring
KF->>FS: Consume claim.fnol_received
FS->>FS: Score claim for fraud indicators
FS->>CS: POST /internal/claims/{id}/fraud-score {score: 0.12, flags: []}
Note over WE: Deadline monitoring runs continuously
WE->>DB: Alert adjuster if ACKNOWLEDGE task approaches due date
Step 5 — Claim State Machine
A claim moves through a precisely defined set of states. The system enforces valid transitions — an adjuster cannot approve a claim that is under fraud investigation, and a closed claim cannot be reopened without supervisor authorization.
stateDiagram-v2
[*] --> FNOL_RECEIVED : Policyholder submits FNOL
FNOL_RECEIVED --> ACKNOWLEDGED : Claims team acknowledges receipt\n(regulatory deadline: 15 days)
FNOL_RECEIVED --> WITHDRAWN : Policyholder withdraws before acknowledgment
ACKNOWLEDGED --> UNDER_INVESTIGATION : Adjuster assigned and begins investigation
ACKNOWLEDGED --> DENIED : Clear exclusion — denied without investigation\n(e.g., policy lapsed)
UNDER_INVESTIGATION --> FRAUD_REVIEW : Fraud score exceeds threshold\nor adjuster flags manually
UNDER_INVESTIGATION --> PENDING_DOCUMENTS : Additional documents requested
UNDER_INVESTIGATION --> EVALUATION : Investigation complete; moving to evaluation
FRAUD_REVIEW --> UNDER_INVESTIGATION : Fraud cleared — return to normal flow
FRAUD_REVIEW --> DENIED : Fraud confirmed — claim denied
PENDING_DOCUMENTS --> UNDER_INVESTIGATION : Documents received
PENDING_DOCUMENTS --> CLOSED_INCOMPLETE : Documents not received by deadline
EVALUATION --> APPROVED : Adjuster approves claim and settlement amount
EVALUATION --> DENIED : Adjuster denies claim (exclusion, no coverage, fraud)
EVALUATION --> SUPERVISOR_REVIEW : Claim above adjuster authority limit\nor policyholder disputes decision
SUPERVISOR_REVIEW --> APPROVED : Supervisor approves
SUPERVISOR_REVIEW --> DENIED : Supervisor upholds denial
APPROVED --> PAYMENT_PROCESSING : Settlement payment initiated
PAYMENT_PROCESSING --> SETTLED : Payment confirmed delivered
PAYMENT_PROCESSING --> PAYMENT_FAILED : Payment failed — retry or alternate method
PAYMENT_FAILED --> PAYMENT_PROCESSING : Retry with corrected bank details
SETTLED --> REOPENED : Supplemental claim or dispute within policy window
REOPENED --> UNDER_INVESTIGATION : Supplemental investigation begins
DENIED --> APPEALED : Policyholder files appeal within appeal window
APPEALED --> SUPERVISOR_REVIEW : Appeal routed to senior adjuster
SETTLED --> CLOSED : Claim fully resolved; final closure
WITHDRAWN --> [*]
CLOSED --> [*]
Valid Transition Enforcement
-- Application-level transition guard
VALID_TRANSITIONS = {
'FNOL_RECEIVED': ['ACKNOWLEDGED', 'WITHDRAWN'],
'ACKNOWLEDGED': ['UNDER_INVESTIGATION', 'DENIED'],
'UNDER_INVESTIGATION': ['FRAUD_REVIEW', 'PENDING_DOCUMENTS', 'EVALUATION'],
'FRAUD_REVIEW': ['UNDER_INVESTIGATION', 'DENIED'],
'PENDING_DOCUMENTS': ['UNDER_INVESTIGATION', 'CLOSED_INCOMPLETE'],
'EVALUATION': ['APPROVED', 'DENIED', 'SUPERVISOR_REVIEW'],
'SUPERVISOR_REVIEW': ['APPROVED', 'DENIED'],
'APPROVED': ['PAYMENT_PROCESSING'],
'PAYMENT_PROCESSING': ['SETTLED', 'PAYMENT_FAILED'],
'PAYMENT_FAILED': ['PAYMENT_PROCESSING'],
'SETTLED': ['REOPENED', 'CLOSED'],
'DENIED': ['APPEALED'],
'APPEALED': ['SUPERVISOR_REVIEW'],
'REOPENED': ['UNDER_INVESTIGATION'],
}
-- Any other transition → raise InvalidClaimTransition exception
Step 6 — Workflow Engine and Task Management
The Workflow Engine is the scheduling brain of the claims system. Every claim generates a set of tasks with deadlines. The engine routes tasks to the right queues, monitors deadlines, and escalates when SLAs are at risk.
Task Types and Deadlines
| Task Type | Assigned To | Regulatory Deadline | Internal SLA Target |
|---|---|---|---|
| Acknowledge FNOL | Claims team | 15 days (most states) | 2 business days |
| Assign adjuster | Claims manager | Internal only | 1 business day |
| Contact policyholder | Assigned adjuster | Internal only | 2 business days |
| Request additional docs | Assigned adjuster | Internal only | Within 15 days |
| Complete field inspection | Field assessor | Internal only | 7–14 business days |
| Complete medical review | Medical reviewer | Internal only | 14 business days |
| Make coverage decision | Adjuster | 30–45 days (most states) | 21 business days |
| Issue payment or denial | Claims team | 5 days after decision | 2 business days |
| Respond to appeal | Senior adjuster | 30 days (most states) | 21 business days |
Deadline Tracking Architecture
flowchart LR
Task["Workflow Task Created\n{task_type, claim_id, due_date}"]
Task --> DB["Stored in workflow_tasks\n(PostgreSQL)"]
Task --> REDIS["Deadline indexed in Redis\nSorted Set: ZADD deadlines {due_timestamp} {task_id}"]
DeadlineMonitor["Deadline Monitor\n(runs every 5 minutes)"]
DeadlineMonitor --> REDIS
REDIS -->|"ZRANGEBYSCORE deadlines NOW() NOW()+24h"| Approaching["Tasks due in < 24 hours"]
Approaching --> AlertAdjuster["Alert assigned adjuster\n(email + portal notification)"]
Approaching --> CheckBreached["Tasks due NOW() or past due"]
CheckBreached --> Escalate["Escalate to supervisor\n+ flag as SLA_AT_RISK"]
CheckBreached --> RegAlert["Regulatory deadline breach?\n→ Compliance team alert\n+ incident record"]
Task Queue Design
Each line of business has separate adjuster queues. Routing considers adjuster workload, specialization, and geography:
flowchart TB
Claim["New Claim\nType: AUTO / PROPERTY / HEALTH\nSeverity: LOW / MEDIUM / HIGH / CAT"]
Router["Routing Engine"]
subgraph Auto["Auto Claims Queues"]
AutoLow["Auto — Low (<$5K)"]
AutoMed["Auto — Medium ($5K–$25K)"]
AutoHigh["Auto — High (>$25K)"]
end
subgraph Property["Property Claims Queues"]
PropLow["Property — Low (<$10K)"]
PropMed["Property — Medium"]
PropHigh["Property — High (>$100K)"]
end
subgraph Health["Health Claims Queues"]
HealthStd["Health — Standard"]
HealthComplex["Health — Complex\n(surgery, chronic)"]
end
CAT["CAT Queue\n(Disaster events — all hands)"]
Claim --> Router
Router --> Auto
Router --> Property
Router --> Health
Router --> CAT
Routing rules:
- Severity determines queue; severity derived from loss description + estimated damage amount
- CAT events (hurricane, tornado, earthquake) create a separate high-priority queue
- Adjusters are pulled from their primary queue; supervisor can manually reassign
- Max queue depth per adjuster: 40 open claims (configurable per jurisdiction)
Step 7 — Document Management
Claims generate a large volume of documents — incident photos, repair estimates, medical records, police reports, contractor invoices. The Document Service handles upload, virus scanning, metadata indexing, and secure retrieval.
Upload Flow
sequenceDiagram
participant P as Policyholder / Adjuster
participant CS as Claims Service
participant DS as Document Service
participant S3 as Object Storage (S3)
participant VS as Virus Scanner
P->>CS: Request upload URL for claim {claim_id, document_type, filename}
CS->>DS: GenerateUploadURL(claim_id, document_type, filename, uploader_id)
DS->>S3: Generate pre-signed PUT URL (expires in 15 minutes)
DS-->>CS: {upload_url, document_id}
CS-->>P: {upload_url, document_id}
P->>S3: PUT file directly to S3 (bypasses backend — no size limit bottleneck)
S3->>DS: S3 Event Notification: object created
DS->>S3: Download file for scanning
DS->>VS: ScanFile(file_bytes)
VS-->>DS: {clean: true} or {infected: true, threat: "Trojan.XYZ"}
alt File is clean
DS->>DS: Extract metadata (EXIF, page count, file hash)
DS->>ClaimDB: INSERT document {claim_id, document_id, status: AVAILABLE, s3_key, metadata}
DS->>KF: Publish claim.document_uploaded {claim_id, document_id, type}
else File is infected
DS->>S3: Delete infected file
DS->>ClaimDB: INSERT document {status: REJECTED_VIRUS}
DS->>KF: Publish claim.document_rejected {claim_id, document_id, reason: VIRUS}
end
Document Types and Retention
| Document Type | Examples | Retention | Access |
|---|---|---|---|
| Incident evidence | Accident photos, property damage photos | 7 years | Adjuster, policyholder |
| Third-party estimates | Auto repair estimate, contractor bid | 7 years | Adjuster, policyholder |
| Medical records | Hospital bills, treatment summaries, Rx | 10 years | Medical reviewer, adjuster (need-to-know) |
| Legal documents | Demand letters, court filings | 10 years | Legal team, senior adjuster |
| Internal assessments | Adjuster notes, inspection reports | 7 years | Adjuster, supervisor |
| Payment records | Settlement agreements, check copies | 10 years | Finance, compliance |
| Correspondence | All emails and letters with policyholder | 7 years | Adjuster, customer service |
Document Security
Storage: S3 with server-side encryption (AES-256)
Access: All retrieval via pre-signed URLs (15-min expiry)
No direct S3 bucket access from client
PHI/PII: Medical documents stored in separate S3 bucket with stricter IAM policy
Access logged to CloudTrail for HIPAA compliance
WORM: S3 Object Lock in COMPLIANCE mode — documents cannot be deleted by anyone
including root account — for regulatory immutability requirement
Audit: Every document access (view, download) logged to audit table
Step 8 — Fraud Detection Integration
Insurance fraud accounts for 10–15% of all claims, costing the industry $40+ billion annually. The fraud detection system runs asynchronously so it does not delay legitimate claim processing.
Fraud Scoring Flow
sequenceDiagram
participant CS as Claims Service
participant KF as Kafka
participant FS as Fraud Service
participant ML as ML Scoring Engine
participant RulesDB as Rules Engine DB
participant DB as Claims DB
CS->>KF: Publish claim.fnol_received {claim_id, policy_data, incident_data}
KF->>FS: Consume event
FS->>ML: ScoreClaim(claim_features)
ML-->>FS: {fraud_score: 0.82, contributing_factors: ["duplicate_incident_location", "policy_recently_increased", "third_claim_in_6_months"]}
FS->>RulesDB: ApplyRules(claim_data, fraud_score)
RulesDB-->>FS: {rule_hits: ["RULE_035: Multiple claims same address 90d", "RULE_078: Policy limit increase <30d before claim"]}
alt Score < 0.3 (low risk)
FS->>DB: UPDATE claim SET fraud_score = 0.12, fraud_status = 'CLEARED'
FS->>KF: Publish claim.fraud_cleared {claim_id}
else Score 0.3–0.7 (medium risk)
FS->>DB: UPDATE claim SET fraud_score = 0.55, fraud_status = 'ELEVATED'
FS->>DB: INSERT fraud_flag {notes: "Elevated score — monitor"}
Note over FS: Claim continues normally but adjuster sees fraud flags
else Score > 0.7 (high risk)
FS->>DB: UPDATE claim SET fraud_score = 0.82, fraud_status = 'UNDER_REVIEW'
FS->>CS: TransitionClaim(claim_id, FRAUD_REVIEW)
FS->>KF: Publish claim.fraud_flagged {claim_id, score, factors}
end
Fraud Indicators (Feature Inputs)
| Category | Indicators |
|---|---|
| Policy history | Policy purchased < 30 days before claim, recent coverage increase, multiple policies same address |
| Claim history | 3+ claims in 12 months, same incident type repeated, prior fraud flag on policyholder |
| Incident patterns | Incident location matches prior claims, incident time/day is statistically anomalous |
| Third-party signals | Repair shop or medical provider flagged for prior fraud, attorney involved from first FNOL |
| Behavioral signals | Claim submitted immediately after accident (no delay), no police report for large theft claim |
| Network analysis | Policyholder connected to known fraud ring (shared addresses, phone numbers, associates) |
Fraud Investigation Queue
Flagged claims enter a special investigation queue visible only to the Special Investigations Unit (SIU):
SIU Queue: Sorted by fraud_score DESC
SLA: SIU must make decision within 30 days of flag
Outcomes:
CLEARED → Claim returns to UNDER_INVESTIGATION state; adjuster notified
CONFIRMED → Claim transitioned to DENIED; legal referral if criminal
INCONCLUSIVE → Claim continues with elevated monitoring; adjuster flagged
Step 9 — Reserve Management
A reserve is the financial estimate of what a claim will ultimately cost the insurer. Reserves are set when a claim is opened and revised as investigation proceeds. Accurate reserves are critical for financial reporting — insurers are legally required to maintain adequate reserves to pay claims.
Reserve Lifecycle
stateDiagram-v2
[*] --> INITIAL_RESERVE : Set automatically at FNOL\nbased on coverage type + initial estimate
INITIAL_RESERVE --> REVISED : Adjuster updates based on\ninspection or medical review
REVISED --> REVISED : Multiple revisions allowed\nas investigation progresses
REVISED --> FINAL_RESERVE : Adjuster sets final amount\nbefore approval
FINAL_RESERVE --> SETTLED_RESERVE : Payment issued;\nreserve closed at actual payment amount
SETTLED_RESERVE --> [*]
Reserve Setting Rules
At FNOL:
Initial reserve = Coverage limit × expected_severity_factor
Auto collision: avg_repair_cost_by_vehicle_type
Property: sq_footage × damage_category_factor
Health: procedure_code_based estimate from medical coding table
Adjuster revision:
Any adjuster can revise reserve up to their authority limit
Revisions above authority → supervisor approval required
Authority limits:
Junior adjuster: Reserve up to $25,000
Senior adjuster: Reserve up to $100,000
Claims supervisor: Reserve up to $500,000
VP Claims: Reserve up to $2,000,000
CFO approval: Reserve > $2,000,000
Reserve history is never overwritten:
Every revision is a new record with timestamp, actor, reason
Finance reports use the current (latest) reserve; history is audit evidence
Reserve Schema
CREATE TABLE claim_reserves (
reserve_id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
claim_id UUID NOT NULL REFERENCES claims(claim_id),
reserve_type VARCHAR(20) NOT NULL, -- 'INDEMNITY' | 'EXPENSE' | 'MEDICAL'
amount BIGINT NOT NULL, -- in cents
set_by VARCHAR(100) NOT NULL, -- user_id or 'SYSTEM'
reason TEXT NOT NULL,
is_current BOOLEAN NOT NULL DEFAULT TRUE,
superseded_at TIMESTAMPTZ, -- set when a newer reserve is created
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Enforce only one current reserve per claim + type
CREATE UNIQUE INDEX idx_reserve_current
ON claim_reserves(claim_id, reserve_type)
WHERE is_current = TRUE;
Step 10 — Settlement and Payment
When a claim is approved, the settlement amount is finalized and payment is disbursed to the policyholder or a designated third party (repair shop, medical provider, attorney).
Settlement Approval Flow
sequenceDiagram
participant Adj as Adjuster
participant CS as Claims Service
participant DB as Claims DB
participant PS as Payment Service
participant KF as Kafka
participant NS as Notification Service
Adj->>CS: POST /v1/claims/{id}/settle\n{settlement_amount, payee, payment_method, notes}
CS->>CS: Validate: settlement_amount <= approved_reserve
CS->>CS: Validate: adjuster authority >= settlement_amount
CS->>DB: BEGIN TRANSACTION
CS->>DB: INSERT settlement_record {amount, payee, method, approved_by}
CS->>DB: UPDATE claim SET status = APPROVED, settlement_amount = :amount
CS->>DB: INSERT claim_event {SETTLEMENT_APPROVED, actor: adjuster_id, amount}
CS->>DB: COMMIT
CS->>KF: Publish claim.settlement_approved {claim_id, amount, payee, method}
KF->>PS: Consume event
PS->>PS: Validate payee bank details / address
PS->>PS: Select payment rail (ACH / wire / check / digital wallet)
PS->>ExternalBank: Initiate payment via payment rail
ExternalBank-->>PS: {payment_reference, estimated_arrival}
PS->>DB: UPDATE claim SET status = PAYMENT_PROCESSING, payment_reference = :ref
PS->>KF: Publish claim.payment_initiated {claim_id, payment_ref, estimated_arrival}
KF->>NS: Send "Your settlement is on the way" to policyholder
Payment Rails
| Payment Method | Processing Time | Use Case | Notes |
|---|---|---|---|
| ACH (direct deposit) | 1–3 business days | Personal settlements | Most common — lowest cost |
| Wire transfer | Same day / next day | Large amounts > $50K | Higher cost; used for speed |
| Paper check | 5–10 business days | Policyholders without bank info | Fraud risk; being deprecated |
| Digital wallet (Zelle / Venmo) | Minutes | Small personal settlements | Age/eligibility restrictions |
| Direct-to-vendor (ACH) | 1–3 business days | Repair shop / medical provider | Third-party payee |
Payment Idempotency
Payments are financial operations. Every payment initiation carries an idempotency key to prevent double-payment on retries:
Idempotency key: payment:{claim_id}:{settlement_id}
Store: PostgreSQL UNIQUE constraint on settlement_records.idempotency_key
Effect: Retry of a payment initiation returns existing payment reference
No duplicate payment is ever sent
Step 11 — Regulatory Compliance Engine
Insurance is heavily regulated. Every U.S. state and many international jurisdictions have specific deadlines for claim acknowledgment, investigation, and settlement. Violation of these deadlines incurs fines and regulatory action.
Compliance Deadline Matrix
| Jurisdiction | Acknowledge | Accept/Deny | Pay After Acceptance |
|---|---|---|---|
| California | 10 days | 30 days | 30 days |
| New York | 15 days | 15 days | 5 days |
| Texas | 15 days | 15 days | 5 days |
| Florida | 14 days | 30 days | 20 days |
| Federal (ERISA health) | 3 days urgent / 15 days standard | 30 days | 15 days |
The system stores the policyholder's state of residence and applies the correct deadline matrix to every claim.
Compliance Monitoring
flowchart TB
Claim["Claim Created\n(state: California)"]
Claim --> DeadlineCalc["Compliance Engine\nCalculates all deadlines\nbased on state + claim type"]
DeadlineCalc --> Deadlines["Deadlines stored:\n• Acknowledge by: Day 10\n• Decision by: Day 30\n• Payment by: Day 60"]
Monitor["Deadline Monitor\n(runs every 15 minutes)"]
Monitor --> D1{"Acknowledge\ndeadline"}
Monitor --> D2{"Decision\ndeadline"}
D1 -->|"< 48h remaining"| Warn["Alert adjuster:\nACKNOWLEDGE DEADLINE IN 48h"]
D1 -->|"< 24h remaining"| Escalate["Alert supervisor\n+ compliance team"]
D1 -->|"BREACHED"| Incident["Create compliance incident\nNotify legal + compliance VP\nPrepare regulatory response"]
D2 -->|"< 72h remaining"| Warn2["Alert adjuster + supervisor"]
D2 -->|"BREACHED"| Incident2["Compliance incident\n+ financial penalty estimate"]
Compliance Incident Record
CREATE TABLE compliance_incidents (
incident_id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
claim_id UUID NOT NULL REFERENCES claims(claim_id),
deadline_type VARCHAR(50) NOT NULL, -- 'ACKNOWLEDGE' | 'DECISION' | 'PAYMENT'
deadline_date DATE NOT NULL,
breached_at TIMESTAMPTZ NOT NULL,
days_overdue INT NOT NULL,
jurisdiction VARCHAR(50) NOT NULL,
penalty_estimate BIGINT, -- in cents
resolution_notes TEXT,
resolved_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
Step 12 — Database Design
Core Tables
-- Master claim record
CREATE TABLE claims (
claim_id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
claim_number VARCHAR(20) NOT NULL UNIQUE, -- human-readable: CLM-2025-0042817
policy_id UUID NOT NULL,
policyholder_id UUID NOT NULL,
line_of_business VARCHAR(20) NOT NULL, -- 'AUTO' | 'PROPERTY' | 'HEALTH' | 'LIABILITY'
incident_date DATE NOT NULL,
incident_type VARCHAR(50) NOT NULL,
description TEXT NOT NULL,
status VARCHAR(30) NOT NULL DEFAULT 'FNOL_RECEIVED',
severity VARCHAR(10) NOT NULL DEFAULT 'MEDIUM', -- LOW | MEDIUM | HIGH | CAT
fraud_score DECIMAL(4,3),
fraud_status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
assigned_adjuster_id UUID,
settlement_amount BIGINT, -- in cents; set on APPROVED
jurisdiction CHAR(2) NOT NULL, -- US state code
idempotency_key VARCHAR(255) UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Immutable claim event log
CREATE TABLE claim_events (
event_id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
claim_id UUID NOT NULL REFERENCES claims(claim_id),
event_type VARCHAR(50) NOT NULL,
from_status VARCHAR(30),
to_status VARCHAR(30),
actor_type VARCHAR(20) NOT NULL, -- 'POLICYHOLDER' | 'ADJUSTER' | 'SYSTEM' | 'SUPERVISOR'
actor_id VARCHAR(100) NOT NULL,
notes TEXT,
metadata JSONB,
occurred_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Workflow tasks with deadlines
CREATE TABLE workflow_tasks (
task_id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
claim_id UUID NOT NULL REFERENCES claims(claim_id),
task_type VARCHAR(50) NOT NULL,
assigned_to UUID, -- user_id of adjuster / assessor
queue_name VARCHAR(50),
status VARCHAR(20) NOT NULL DEFAULT 'OPEN',
priority INT NOT NULL DEFAULT 5,
due_date TIMESTAMPTZ NOT NULL,
regulatory_deadline TIMESTAMPTZ, -- hard deadline (regulatory vs internal SLA)
completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Document metadata
CREATE TABLE claim_documents (
document_id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
claim_id UUID NOT NULL REFERENCES claims(claim_id),
document_type VARCHAR(50) NOT NULL,
filename VARCHAR(255) NOT NULL,
s3_key VARCHAR(500) NOT NULL,
file_size_bytes BIGINT NOT NULL,
mime_type VARCHAR(100),
file_hash VARCHAR(64), -- SHA-256 for integrity verification
uploaded_by VARCHAR(100) NOT NULL,
uploader_type VARCHAR(20) NOT NULL, -- 'POLICYHOLDER' | 'ADJUSTER' | 'ASSESSOR'
virus_scan_status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
status VARCHAR(20) NOT NULL DEFAULT 'PROCESSING',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Settlement records
CREATE TABLE settlements (
settlement_id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
claim_id UUID NOT NULL REFERENCES claims(claim_id),
settlement_amount BIGINT NOT NULL, -- in cents
payee_name VARCHAR(200) NOT NULL,
payee_type VARCHAR(20) NOT NULL, -- 'POLICYHOLDER' | 'REPAIR_SHOP' | 'MEDICAL_PROVIDER'
payment_method VARCHAR(20) NOT NULL,
payment_reference VARCHAR(100),
payment_status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
idempotency_key VARCHAR(255) NOT NULL UNIQUE,
approved_by VARCHAR(100) NOT NULL,
approved_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
paid_at TIMESTAMPTZ
);
Key Indexes
-- Claims by policyholder (customer portal)
CREATE INDEX idx_claims_policyholder ON claims(policyholder_id, created_at DESC);
-- Claims by adjuster (adjuster dashboard)
CREATE INDEX idx_claims_adjuster ON claims(assigned_adjuster_id, status)
WHERE assigned_adjuster_id IS NOT NULL;
-- Open tasks approaching deadline (workflow engine)
CREATE INDEX idx_tasks_deadline ON workflow_tasks(due_date, status)
WHERE status = 'OPEN';
-- Claims by status + line (queue management)
CREATE INDEX idx_claims_status_lob ON claims(status, line_of_business, created_at);
-- Fraud review queue
CREATE INDEX idx_claims_fraud_review ON claims(fraud_status, fraud_score DESC)
WHERE fraud_status IN ('ELEVATED', 'UNDER_REVIEW');
-- Document lookup by claim
CREATE INDEX idx_documents_claim ON claim_documents(claim_id, created_at DESC);
-- Events per claim (timeline view)
CREATE INDEX idx_events_claim ON claim_events(claim_id, occurred_at DESC);
Step 13 — API Design
Submit FNOL
POST /v1/claims/fnol
{
"idempotency_key": "fnol-policy-P12345-2025-03-01T10:00:00Z",
"policy_number": "POL-P12345",
"incident_date": "2025-02-28",
"incident_time": "14:30",
"incident_type": "auto_collision",
"incident_location": "123 Main St, San Francisco, CA 94105",
"description": "Rear-ended at a red light. Significant trunk damage.",
"at_fault": false,
"third_party_involved": true,
"police_report_number": "SFPD-2025-00314"
}
201 Created response:
{
"claim_id": "clm_9f8e7d6c5b4a",
"claim_number": "CLM-2025-0042817",
"status": "fnol_received",
"next_steps": "A claims adjuster will contact you within 2 business days.",
"acknowledgment_deadline": "2025-03-13",
"contact_info": { "phone": "1-800-CLAIMS-1", "email": "[email protected]" },
"created_at": "2025-03-01T10:00:02Z"
}
Get Claim Status
GET /v1/claims/{claim_id}
{
"claim_id": "clm_9f8e7d6c5b4a",
"claim_number": "CLM-2025-0042817",
"status": "under_investigation",
"line_of_business": "auto",
"incident_date": "2025-02-28",
"adjuster": {
"name": "Sarah Mitchell",
"phone": "415-555-0192",
"email": "[email protected]"
},
"current_reserve": 12500,
"documents_requested": [
{ "type": "repair_estimate", "status": "pending", "due_date": "2025-03-15" }
],
"timeline": [
{ "event": "FNOL submitted", "date": "2025-03-01T10:00:02Z" },
{ "event": "Claim acknowledged", "date": "2025-03-03T09:15:00Z" },
{ "event": "Adjuster assigned", "date": "2025-03-03T09:15:00Z" },
{ "event": "Investigation started","date": "2025-03-04T11:00:00Z" }
]
}
Update Claim Status (Adjuster)
POST /v1/claims/{claim_id}/transitions
{
"to_status": "evaluation",
"notes": "Field inspection complete. Damage consistent with reported incident. Moving to evaluation.",
"actor_id": "adj_sarah_mitchell"
}
Request Documents
POST /v1/claims/{claim_id}/document-requests
{
"document_type": "medical_records",
"instructions": "Please upload hospital records and itemized bills for the ER visit on 2025-02-28.",
"due_date": "2025-03-15",
"required": true
}
Step 14 — Kafka Event Architecture
flowchart TB
CS["Claims Service"]
CS --> T1["claim.fnol_received"]
CS --> T2["claim.status_changed"]
CS --> T3["claim.document_uploaded"]
CS --> T4["claim.settlement_approved"]
CS --> T5["claim.deadline_approaching"]
CS --> T6["claim.deadline_breached"]
T1 --> FS["Fraud Service\n(score new claims)"]
T1 --> NS["Notification Service\n(FNOL confirmation)"]
T1 --> AS["Audit Service\n(log FNOL event)"]
T2 --> NS["Notification Service\n(status updates)"]
T2 --> AS["Audit Service\n(all transitions)"]
T2 --> Analytics["Analytics\n(claim pipeline metrics)"]
T3 --> DS["Document Service\n(virus scan trigger)"]
T3 --> WE["Workflow Engine\n(mark doc request fulfilled)"]
T4 --> PS["Payment Service\n(initiate settlement)"]
T4 --> RS["Reserve Service\n(close reserve)"]
T4 --> AS["Audit Service"]
T5 --> NS["Alert adjuster\n+ supervisor"]
T6 --> Compliance["Compliance Team\n(incident creation)"]
Step 15 — Failure Handling and Observability
CAT Event Handling (Catastrophic Events)
A hurricane, wildfire, or earthquake can generate thousands of FNOLs in hours — 50× normal volume. The system must absorb this without losing any claim.
flowchart LR
CAT["CAT Event\n(hurricane Ian — 50,000 claims in 24h)"]
CAT --> KF["Kafka\nFNOL Topic\n(buffers surge)"]
KF --> FNOL_Workers["FNOL Processing Workers\n(auto-scale on consumer lag)"]
FNOL_Workers --> DB["PostgreSQL\n(writes — all claims persisted)"]
CAT --> CAT_Flag["CAT Mode Enabled\n(operations manager toggles)"]
CAT_Flag --> AdjRouting["Route all new claims\nto CAT Queue\n(not regular queues)"]
CAT_Flag --> TempAdjusters["Enable CAT adjusters\n(on-call pool + contractors)"]
CAT_Flag --> ExtendDeadlines["Regulatory deadline extension\n(most states allow CAT extensions)"]
Key Metrics
| Metric | Description | Alert Threshold |
|---|---|---|
claims.fnol_success_rate |
% FNOLs successfully created | < 99.5% |
claims.fnol_latency_p99 |
FNOL submission e2e latency | > 5 seconds |
claims.open_tasks_overdue |
Tasks past their internal SLA | > 50 (escalate) |
claims.regulatory_deadlines_at_risk |
Tasks within 48h of regulatory deadline | Any |
claims.regulatory_deadline_breaches |
Confirmed regulatory breaches | Any — P0 alert |
claims.fraud_review_backlog |
Claims in FRAUD_REVIEW > 30 days | > 0 |
claims.adjuster_queue_depth |
Open claims per adjuster | > 40 (overloaded) |
documents.virus_scan_failure_rate |
Failed virus scans | > 0.1% |
payments.failure_rate |
Settlement payment failures | > 1% |
kafka.consumer_lag |
Lag on claim event topics | > 10,000 messages |
Claims Lifecycle Dashboard
The operations team monitors a real-time view of claim pipeline health:
| Stage | Count | Avg Age | % Past SLA |
|---|---|---|---|
| FNOL Received | 842 | 0.3 days | 0% |
| Acknowledged | 1,204 | 1.2 days | 0% |
| Under Investigation | 48,320 | 8.4 days | 2.1% |
| Fraud Review | 387 | 14.2 days | 12% ⚠️ |
| Pending Documents | 3,102 | 6.8 days | 4.3% |
| Evaluation | 8,940 | 18.1 days | 1.8% |
| Approved / Payment | 2,104 | 22.4 days | 0.8% |
| Total Active | 64,899 | 10.2 days avg | 2.4% overall |
Design Trade-offs
Trade-off 1: Workflow State — Database vs Dedicated Workflow Engine
| Approach | Pros | Cons |
|---|---|---|
| Database-backed state machine (chosen) | Simple; no additional infrastructure; state is durable by default | Custom deadline tracking; no visual workflow designer |
| Dedicated workflow engine (Temporal / Camunda) | Built-in deadline management; visual workflow; durable execution | Operational complexity; vendor dependency; learning curve |
Decision: Database-backed state machine for v1. The claim lifecycle is well-defined and stable — it does not change frequently. A bespoke state machine in PostgreSQL is simpler to operate, easier to debug, and has no external dependency. Temporal or Camunda becomes worth the complexity if the workflow needs frequent reconfiguration or branching logic exceeds ~15 states.
Trade-off 2: Synchronous vs Asynchronous Fraud Scoring
| Approach | Pros | Cons |
|---|---|---|
| Asynchronous via Kafka (chosen) | FNOL response is fast; fraud scoring does not block claim creation | Small window where claim is in system before fraud score is applied |
| Synchronous in FNOL path | Fraud score available before claim is acknowledged | Adds 500ms–2s to FNOL response; ML model latency is unpredictable |
Decision: Asynchronous. The policyholder submitting a FNOL is in distress — a 3-second response is already the limit. Fraud scoring typically completes in under 5 seconds via Kafka and catches all fraud before any human action is taken on the claim.
Trade-off 3: Object Storage for Documents vs Database BLOBs
| Approach | Pros | Cons |
|---|---|---|
| Object storage — S3 (chosen) | Unlimited scale; built-in WORM/compliance; pre-signed URLs for direct access | Additional infrastructure; requires metadata in DB separately |
| Database BLOBs (PostgreSQL large objects) | Single system; simpler architecture | Massive DB size; backup complexity; no built-in WORM compliance |
Decision: Object storage always. At 400 GB/day, storing files in the database would be catastrophically expensive and slow. S3's built-in Object Lock provides WORM compliance required for regulatory retention. Pre-signed URLs let clients download directly without routing through the backend.
Trade-off 4: Single Claims Service vs Line-of-Business Microservices
| Approach | Pros | Cons |
|---|---|---|
| Single Claims Service (chosen) | Shared workflow engine; unified audit log; simpler ops | Auto/health/property have different fields and rules in same codebase |
| Per-LOB microservices | Independent deployment per line; different schemas | Shared audit trail becomes complex; routing logic split across services |
Decision: Single Claims Service with line-of-business as a first-class field. The claim lifecycle is largely the same across lines (FNOL → investigation → decision → payment). LOB-specific logic lives in pluggable rules and field validation, not separate services. This avoids duplicating the workflow engine, compliance monitoring, and audit service.
Common Interview Mistakes
| Mistake | Why It Matters |
|---|---|
| Not modeling the claim as a state machine | Without explicit valid transitions, adjusters can approve denied claims, settle fraud cases, and create invalid claim states |
| Storing documents in the claims database | At 400 GB/day, the database becomes unusably large; backup and restore times become unacceptable |
| Missing regulatory deadline tracking | Deadline breaches generate automatic fines; a system that cannot track its own deadlines is legally non-compliant |
| Synchronous fraud scoring in the FNOL path | Adds unpredictable latency to the most time-critical API call; fraud can be scored asynchronously without risk |
| Not distinguishing reserve from settlement amount | Reserve is an estimate; settlement is the final actual payment; conflating them corrupts financial reporting |
| Mutable audit log | In litigation or regulatory examination, if the audit log can be edited, it has zero legal weight |
| Missing payment idempotency | A payment service retry after a timeout can double-pay a policyholder — a significant financial and regulatory violation |
| Ignoring CAT event scaling | Normal claim volume is low; a single weather event generates 50× volume; architecture must absorb this via queuing, not synchronous processing |
Summary
The Insurance Claims System is one of the most regulated and operationally complex enterprise systems to design. It combines a long-lived workflow engine, financial reserve management, multi-party document exchange, fraud detection, and strict regulatory compliance — all under a mandate that no claim can be lost and no deadline can be missed.
flowchart TB
Core["Insurance Claims System"]
Core --> Lifecycle["Claim Lifecycle\nState machine\nFNOL → Settlement"]
Core --> Workflow["Workflow Engine\nTask routing\nDeadline tracking"]
Core --> Docs["Document Management\nS3 WORM storage\nPre-signed URLs"]
Core --> Fraud["Fraud Detection\nAsync ML scoring\nSIU investigation queue"]
Core --> Reserves["Reserve Management\nFinancial estimates\nAuthority limits"]
Core --> Compliance["Regulatory Compliance\nJurisdiction deadlines\nBreached → P0 alert"]
The three design principles that make a claims system reliable:
-
The audit log is the system's legal record — every action on every claim is permanently recorded with actor, timestamp, and reason. If it is not in the audit log, it did not happen. This is not a feature; it is a compliance obligation.
-
Regulatory deadlines are P0 concerns — a single missed acknowledgment deadline can trigger a state regulatory investigation. Deadline monitoring must run continuously, alert early, and escalate aggressively. There is no acceptable failure mode.
-
Decouple long-running workflows from synchronous APIs — the FNOL response must be fast because the policyholder is in distress. Fraud scoring, document processing, compliance checks, and payment processing all happen asynchronously. The claim exists and is tracked the moment the FNOL is submitted.